robot_lab 0.2.1 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.envrc +1 -0
- data/.loki +60 -0
- data/.quality/reek_baseline.txt +43 -0
- data/.rubocop.yml +5 -167
- data/CHANGELOG.md +54 -0
- data/README.md +185 -64
- data/Rakefile +28 -12
- data/docs/api/core/index.md +41 -14
- data/docs/api/core/memory.md +247 -29
- data/docs/api/core/network.md +285 -33
- data/docs/api/core/result.md +120 -32
- data/docs/api/core/robot.md +636 -60
- data/docs/api/core/state.md +87 -197
- data/docs/api/core/tool.md +165 -20
- data/docs/api/errors.md +152 -14
- data/docs/api/hooks.md +469 -0
- data/docs/api/index.md +83 -7
- data/docs/api/mcp/client.md +129 -35
- data/docs/api/mcp/index.md +164 -23
- data/docs/api/mcp/server.md +27 -3
- data/docs/api/mcp/transports.md +94 -22
- data/docs/api/messages/index.md +26 -3
- data/docs/api/messages/text-message.md +33 -11
- data/docs/api/messages/tool-call-message.md +27 -4
- data/docs/api/messages/tool-result-message.md +23 -4
- data/docs/api/messages/user-message.md +45 -8
- data/docs/api/skills.md +519 -0
- data/docs/api/streaming/context.md +28 -5
- data/docs/api/streaming/index.md +57 -11
- data/docs/api/support.md +846 -0
- data/docs/architecture/core-concepts.md +118 -30
- data/docs/architecture/index.md +86 -11
- data/docs/architecture/message-flow.md +66 -29
- data/docs/architecture/network-orchestration.md +153 -38
- data/docs/architecture/robot-execution.md +173 -91
- data/docs/architecture/state-management.md +31 -12
- data/docs/concepts.md +176 -21
- data/docs/examples/basic-chat.md +72 -19
- data/docs/examples/index.md +117 -31
- data/docs/examples/mcp-server.md +154 -45
- data/docs/examples/multi-robot-network.md +91 -21
- data/docs/examples/tool-usage.md +104 -37
- data/docs/getting-started/configuration.md +299 -93
- data/docs/getting-started/installation.md +53 -41
- data/docs/getting-started/quick-start.md +51 -6
- data/docs/guides/building-robots.md +296 -50
- data/docs/guides/creating-networks.md +220 -31
- data/docs/guides/hooks.md +1084 -0
- data/docs/guides/index.md +5 -0
- data/docs/guides/knowledge.md +37 -6
- data/docs/guides/mcp-integration.md +211 -44
- data/docs/guides/memory.md +103 -12
- data/docs/guides/observability.md +166 -51
- data/docs/guides/streaming.md +184 -125
- data/docs/guides/using-tools.md +300 -11
- data/docs/index.md +37 -5
- data/examples/01_simple_robot.rb +2 -2
- data/examples/02_tools.rb +14 -4
- data/examples/03_network.rb +23 -9
- data/examples/04_mcp.rb +11 -4
- data/examples/05_streaming.rb +8 -5
- data/examples/06_prompt_templates.rb +13 -9
- data/examples/07_network_memory.rb +5 -5
- data/examples/08_llm_config.rb +20 -15
- data/examples/09_chaining.rb +4 -4
- data/examples/11_network_introspection.rb +4 -4
- data/examples/12_message_bus.rb +2 -2
- data/examples/13_spawn.rb +2 -2
- data/examples/14_rusty_circuit/README.md +1 -0
- data/examples/14_rusty_circuit/comic.rb +7 -3
- data/examples/14_rusty_circuit/display.rb +14 -0
- data/examples/14_rusty_circuit/heckler.rb +8 -6
- data/examples/14_rusty_circuit/open_mic.rb +17 -6
- data/examples/14_rusty_circuit/scout.rb +17 -10
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +14 -10
- data/examples/15_memory_network_and_bus/linux_writer.rb +2 -2
- data/examples/15_memory_network_and_bus/os_editor.rb +3 -1
- data/examples/15_memory_network_and_bus/os_writer.rb +4 -1
- data/examples/16_writers_room/writer.rb +22 -22
- data/examples/16_writers_room/writers_room.rb +2 -0
- data/examples/17_skills.rb +14 -13
- data/examples/18_rails/README.md +20 -1
- data/examples/18_rails/app/controllers/chat_controller.rb +5 -1
- data/examples/18_rails/app/jobs/robot_run_job.rb +11 -5
- data/examples/18_rails/app/robots/chat_robot.rb +11 -0
- data/examples/18_rails/config/initializers/robot_lab.rb +8 -0
- data/examples/19_token_tracking.rb +25 -9
- data/examples/20_circuit_breaker.rb +10 -7
- data/examples/21_learning_loop.rb +42 -16
- data/examples/22_context_compression.rb +23 -23
- data/examples/23_convergence.rb +24 -17
- data/examples/24_structured_delegation.rb +13 -8
- data/examples/25_history_search.rb +12 -8
- data/examples/27_incident_response/incident_response.rb +31 -13
- data/examples/28_mcp_discovery.rb +17 -13
- data/examples/29_ractor_tools.rb +4 -2
- data/examples/30_ractor_network.rb +22 -17
- data/examples/31_launch_assessment.rb +20 -9
- data/examples/32_newsletter_reader.rb +7 -2
- data/examples/33_stock_predictor.rb +34 -13
- data/examples/34_agentskills.rb +7 -3
- data/examples/35_hooks.rb +266 -0
- data/examples/README.md +203 -42
- data/examples/common.rb +79 -7
- data/examples/xyzzy.rb +97 -0
- data/lib/robot_lab/agent_skill.rb +5 -4
- data/lib/robot_lab/budget/ledger.rb +98 -0
- data/lib/robot_lab/capabilities.rb +84 -0
- data/lib/robot_lab/config/defaults.yml +10 -0
- data/lib/robot_lab/config.rb +14 -9
- data/lib/robot_lab/error.rb +34 -4
- data/lib/robot_lab/errors.rb +45 -0
- 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/connection_poller.rb +2 -2
- data/lib/robot_lab/names.rb +402 -0
- data/lib/robot_lab/narrator.rb +87 -0
- data/lib/robot_lab/network.rb +77 -20
- data/lib/robot_lab/robot/agent_skill_matching.rb +1 -3
- data/lib/robot_lab/robot/budget.rb +89 -0
- data/lib/robot_lab/robot/bus_messaging.rb +90 -18
- data/lib/robot_lab/robot/hooking.rb +56 -0
- data/lib/robot_lab/robot/mcp_management.rb +8 -2
- data/lib/robot_lab/robot/template_rendering.rb +18 -5
- data/lib/robot_lab/robot.rb +233 -89
- data/lib/robot_lab/run_config.rb +24 -5
- 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 +60 -2
- data/lib/robot_lab/task.rb +26 -20
- data/lib/robot_lab/tool.rb +52 -11
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +70 -15
- data/mkdocs.yml +7 -1
- metadata +41 -99
- data/examples/temp.md +0 -51
- data/site/404.html +0 -2300
- data/site/api/core/index.html +0 -2706
- data/site/api/core/memory/index.html +0 -3793
- data/site/api/core/network/index.html +0 -3500
- data/site/api/core/robot/index.html +0 -4566
- data/site/api/core/state/index.html +0 -3390
- data/site/api/core/tool/index.html +0 -3843
- data/site/api/index.html +0 -2635
- data/site/api/mcp/client/index.html +0 -3435
- data/site/api/mcp/index.html +0 -2783
- data/site/api/mcp/server/index.html +0 -3252
- data/site/api/mcp/transports/index.html +0 -3352
- data/site/api/messages/index.html +0 -2641
- data/site/api/messages/text-message/index.html +0 -3087
- data/site/api/messages/tool-call-message/index.html +0 -3159
- data/site/api/messages/tool-result-message/index.html +0 -3252
- data/site/api/messages/user-message/index.html +0 -3212
- data/site/api/streaming/context/index.html +0 -3282
- data/site/api/streaming/events/index.html +0 -3347
- data/site/api/streaming/index.html +0 -2738
- data/site/architecture/core-concepts/index.html +0 -3757
- data/site/architecture/index.html +0 -2797
- data/site/architecture/message-flow/index.html +0 -3238
- data/site/architecture/network-orchestration/index.html +0 -3433
- data/site/architecture/robot-execution/index.html +0 -3140
- data/site/architecture/state-management/index.html +0 -3498
- data/site/assets/css/custom.css +0 -56
- data/site/assets/images/favicon.png +0 -0
- data/site/assets/images/robot_lab.jpg +0 -0
- data/site/assets/javascripts/bundle.79ae519e.min.js +0 -16
- data/site/assets/javascripts/bundle.79ae519e.min.js.map +0 -7
- data/site/assets/javascripts/lunr/min/lunr.ar.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.da.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.de.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.du.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.el.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.es.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.fi.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.fr.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.he.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.hi.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.hu.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.hy.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.it.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ja.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.jp.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.kn.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.ko.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.multi.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.nl.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.no.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.pt.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ro.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ru.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.sa.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.sv.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ta.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.te.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.th.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.tr.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.vi.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.zh.min.js +0 -1
- data/site/assets/javascripts/lunr/tinyseg.js +0 -206
- data/site/assets/javascripts/lunr/wordcut.js +0 -6708
- data/site/assets/javascripts/workers/search.2c215733.min.js +0 -42
- data/site/assets/javascripts/workers/search.2c215733.min.js.map +0 -7
- data/site/assets/stylesheets/main.484c7ddc.min.css +0 -1
- data/site/assets/stylesheets/main.484c7ddc.min.css.map +0 -1
- data/site/assets/stylesheets/palette.ab4e12ef.min.css +0 -1
- data/site/assets/stylesheets/palette.ab4e12ef.min.css.map +0 -1
- data/site/concepts/index.html +0 -3455
- data/site/examples/basic-chat/index.html +0 -2880
- data/site/examples/index.html +0 -2907
- data/site/examples/mcp-server/index.html +0 -3018
- data/site/examples/multi-robot-network/index.html +0 -3131
- data/site/examples/rails-application/index.html +0 -3329
- data/site/examples/tool-usage/index.html +0 -3085
- data/site/getting-started/configuration/index.html +0 -3745
- data/site/getting-started/index.html +0 -2572
- data/site/getting-started/installation/index.html +0 -2981
- data/site/getting-started/quick-start/index.html +0 -2942
- data/site/guides/building-robots/index.html +0 -4290
- data/site/guides/creating-networks/index.html +0 -3858
- data/site/guides/index.html +0 -2586
- data/site/guides/mcp-integration/index.html +0 -3581
- data/site/guides/memory/index.html +0 -3586
- data/site/guides/rails-integration/index.html +0 -4019
- data/site/guides/streaming/index.html +0 -3157
- data/site/guides/using-tools/index.html +0 -3802
- data/site/index.html +0 -2671
- data/site/search/search_index.json +0 -1
- data/site/sitemap.xml +0 -183
- data/site/sitemap.xml.gz +0 -0
- data/site/tags.json +0 -1
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
# - Contrast with bus messaging (fire-and-forget) and pipelines (predefined)
|
|
19
19
|
#
|
|
20
20
|
# Usage:
|
|
21
|
-
#
|
|
21
|
+
# ruby examples/24_structured_delegation.rb
|
|
22
22
|
|
|
23
23
|
require_relative "common"
|
|
24
24
|
|
|
@@ -28,19 +28,19 @@ banner "Structured Delegation"
|
|
|
28
28
|
# Build a manager and two specialist robots
|
|
29
29
|
# ---------------------------------------------------------------------------
|
|
30
30
|
manager = RobotLab.build(
|
|
31
|
-
|
|
31
|
+
**llm_opts,
|
|
32
32
|
name: "manager",
|
|
33
33
|
system_prompt: "You are a project manager. Delegate tasks concisely."
|
|
34
34
|
)
|
|
35
35
|
|
|
36
36
|
summarizer = RobotLab.build(
|
|
37
|
-
|
|
37
|
+
**llm_opts,
|
|
38
38
|
name: "summarizer",
|
|
39
39
|
system_prompt: "You are a concise summarizer. Produce a 1-2 sentence summary."
|
|
40
40
|
)
|
|
41
41
|
|
|
42
42
|
analyst = RobotLab.build(
|
|
43
|
-
|
|
43
|
+
**llm_opts,
|
|
44
44
|
name: "analyst",
|
|
45
45
|
system_prompt: "You are a data analyst. Identify the key metric in one sentence."
|
|
46
46
|
)
|
|
@@ -89,12 +89,12 @@ section "Asynchronous (parallel fan-out)"
|
|
|
89
89
|
|
|
90
90
|
# Fresh robots — each delegate call should start from a clean slate
|
|
91
91
|
async_summarizer = RobotLab.build(
|
|
92
|
-
|
|
92
|
+
**llm_opts,
|
|
93
93
|
name: "summarizer",
|
|
94
94
|
system_prompt: "You are a concise summarizer. Produce a 1-2 sentence summary."
|
|
95
95
|
)
|
|
96
96
|
async_analyst = RobotLab.build(
|
|
97
|
-
|
|
97
|
+
**llm_opts,
|
|
98
98
|
name: "analyst",
|
|
99
99
|
system_prompt: "You are a data analyst. Identify the key metric in one sentence."
|
|
100
100
|
)
|
|
@@ -109,8 +109,13 @@ puts "Both futures launched. Futures resolved? " \
|
|
|
109
109
|
"summary=#{f_summary.resolved?} analysis=#{f_analysis.resolved?}"
|
|
110
110
|
puts "Collecting results..."
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
# value(timeout:) raises DelegationFuture::DelegationTimeout if the delegatee
|
|
113
|
+
# hasn't finished in time. A local model is much slower than a hosted one, so
|
|
114
|
+
# budget generously — 60s is comfortably too short for a 20B+ model.
|
|
115
|
+
DELEGATION_TIMEOUT = Integer(ENV.fetch("DELEGATION_TIMEOUT", "600"))
|
|
116
|
+
|
|
117
|
+
summary = f_summary.value(timeout: DELEGATION_TIMEOUT)
|
|
118
|
+
analysis = f_analysis.value(timeout: DELEGATION_TIMEOUT)
|
|
114
119
|
|
|
115
120
|
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
|
|
116
121
|
|
|
@@ -23,17 +23,21 @@ CONVERSATION_TURNS = File.readlines(
|
|
|
23
23
|
banner "Chat History Search"
|
|
24
24
|
|
|
25
25
|
# ---------------------------------------------------------------------------
|
|
26
|
-
#
|
|
26
|
+
# Build a robot and load the conversation fixture
|
|
27
|
+
#
|
|
28
|
+
# Robot#replace_messages is the supported way to install a saved conversation
|
|
29
|
+
# — checkpoint restore, replaying a transcript, or seeding a fixture like this
|
|
30
|
+
# one. It resets the chat and re-adds each message, so there is no need to
|
|
31
|
+
# reach into @chat and overwrite @messages by hand.
|
|
27
32
|
# ---------------------------------------------------------------------------
|
|
28
|
-
|
|
33
|
+
robot = RobotLab.build(**llm_opts, name: "tech_lead", system_prompt: "You are a senior engineering advisor.")
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
robot
|
|
35
|
+
messages = CONVERSATION_TURNS.map do |turn|
|
|
36
|
+
RubyLLM::Message.new(role: turn[:role].to_sym, content: turn[:content])
|
|
37
|
+
end
|
|
38
|
+
robot.replace_messages(messages)
|
|
34
39
|
|
|
35
|
-
messages =
|
|
36
|
-
robot.instance_variable_get(:@chat).instance_variable_set(:@messages, messages)
|
|
40
|
+
messages = robot.messages
|
|
37
41
|
|
|
38
42
|
total_words = messages.sum { |m| m.content.to_s.split.size }
|
|
39
43
|
puts "Conversation loaded: #{messages.size} messages, ~#{total_words} words"
|
|
@@ -60,7 +60,7 @@ class SREScout < RobotLab::Robot
|
|
|
60
60
|
def initialize(name:, subsystem:, memory_key:, bus: nil)
|
|
61
61
|
super(
|
|
62
62
|
name: name,
|
|
63
|
-
|
|
63
|
+
**llm_opts,
|
|
64
64
|
system_prompt: "You are a senior SRE responding to a production outage. " \
|
|
65
65
|
"Diagnose one infrastructure layer in 2 sentences: " \
|
|
66
66
|
"first sentence is root cause, second is customer impact.",
|
|
@@ -111,9 +111,8 @@ class WarRoom < RobotLab::Robot
|
|
|
111
111
|
attr_reader :updates
|
|
112
112
|
|
|
113
113
|
def initialize(bus:)
|
|
114
|
-
super(name: "war_room",
|
|
115
|
-
@updates
|
|
116
|
-
@delivery_mutex = Mutex.new # only for reading @updates outside Async
|
|
114
|
+
super(name: "war_room", **llm_opts, system_prompt: "SRE war-room coordinator.", bus: bus)
|
|
115
|
+
@updates = []
|
|
117
116
|
|
|
118
117
|
on_message do |msg|
|
|
119
118
|
# BusPoller ensures this block is never re-entered for the same robot.
|
|
@@ -137,15 +136,12 @@ end
|
|
|
137
136
|
class IncidentCommander < RobotLab::Robot
|
|
138
137
|
attr_writer :shared_memory
|
|
139
138
|
|
|
139
|
+
FINDING_KEYS = %i[db_finding net_finding app_finding].freeze
|
|
140
|
+
|
|
140
141
|
def call(result)
|
|
141
142
|
puts " [commander] Blocking on reactive memory — waiting for all scout findings..."
|
|
142
143
|
|
|
143
|
-
findings =
|
|
144
|
-
|
|
145
|
-
if findings.values.include?(:timeout)
|
|
146
|
-
timed_out = findings.select { |_k, v| v == :timeout }.keys
|
|
147
|
-
puts " [commander] WARNING: scouts timed out: #{timed_out.join(", ")}"
|
|
148
|
-
end
|
|
144
|
+
findings = collect_findings
|
|
149
145
|
|
|
150
146
|
prompt = <<~PROMPT
|
|
151
147
|
Three SRE scouts have reported their findings for a payment-service outage.
|
|
@@ -167,6 +163,24 @@ class IncidentCommander < RobotLab::Robot
|
|
|
167
163
|
|
|
168
164
|
result.with_context(:commander, report).continue(report)
|
|
169
165
|
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
# Blocking multi-key read with a real degradation path.
|
|
170
|
+
#
|
|
171
|
+
# Memory#get(wait:) signals a timeout by RAISING RobotLab::AwaitTimeout — it
|
|
172
|
+
# never returns a :timeout sentinel value. Rescue it and re-read without
|
|
173
|
+
# wait:, which never blocks and simply omits keys that were never written,
|
|
174
|
+
# so a slow scout degrades the report instead of killing the commander.
|
|
175
|
+
def collect_findings
|
|
176
|
+
@shared_memory.get(*FINDING_KEYS, wait: 60)
|
|
177
|
+
rescue RobotLab::AwaitTimeout => e
|
|
178
|
+
puts " [commander] WARNING: #{e.message}"
|
|
179
|
+
partial = @shared_memory.get(*FINDING_KEYS)
|
|
180
|
+
missing = FINDING_KEYS - partial.keys
|
|
181
|
+
puts " [commander] Proceeding without: #{missing.join(', ')}" if missing.any?
|
|
182
|
+
partial
|
|
183
|
+
end
|
|
170
184
|
end
|
|
171
185
|
|
|
172
186
|
# ── Wire up ────────────────────────────────────────────────────────────────
|
|
@@ -177,7 +191,7 @@ db_scout = SREScout.new(name: "db_scout", subsystem: "database", memory_key
|
|
|
177
191
|
net_scout = SREScout.new(name: "net_scout", subsystem: "network", memory_key: :net_finding, bus: bus)
|
|
178
192
|
app_scout = SREScout.new(name: "app_scout", subsystem: "application", memory_key: :app_finding, bus: bus)
|
|
179
193
|
war_room = WarRoom.new(bus: bus)
|
|
180
|
-
commander = IncidentCommander.new(name: "commander",
|
|
194
|
+
commander = IncidentCommander.new(name: "commander", **llm_opts, system_prompt: "SRE incident commander.")
|
|
181
195
|
|
|
182
196
|
# Build the investigation network
|
|
183
197
|
# poller_group: labels are registered on the network's shared BusPoller.
|
|
@@ -212,8 +226,12 @@ puts
|
|
|
212
226
|
puts network.visualize
|
|
213
227
|
puts
|
|
214
228
|
|
|
215
|
-
puts "Poller groups
|
|
216
|
-
puts "
|
|
229
|
+
puts "Poller groups declared on the network's tasks:"
|
|
230
|
+
puts " :investigation — db_scout, net_scout, app_scout"
|
|
231
|
+
puts " :command — commander"
|
|
232
|
+
puts "(Network#task registers each group on the shared BusPoller; the poller"
|
|
233
|
+
puts " itself is internal, so the groups are documented here rather than"
|
|
234
|
+
puts " introspected.)"
|
|
217
235
|
puts
|
|
218
236
|
|
|
219
237
|
puts "INCIDENT: Payment service degraded — elevated error rates and timeouts"
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
# == Key config
|
|
13
13
|
#
|
|
14
14
|
# robot = RobotLab.build(
|
|
15
|
-
# model: "
|
|
15
|
+
# provider: "ollama", model: "qwen3.6:latest",
|
|
16
16
|
# mcp_discovery: true, # ← enables semantic filtering
|
|
17
17
|
# mcp: [ ... ] # ← candidate servers, each with :description
|
|
18
18
|
# )
|
|
@@ -55,8 +55,9 @@ def show_query(label, query)
|
|
|
55
55
|
selected = RobotLab::MCP::ServerDiscovery.select(query, from: SERVERS)
|
|
56
56
|
names = selected.map { |s| s[:name] }
|
|
57
57
|
|
|
58
|
-
puts "
|
|
59
|
-
puts "
|
|
58
|
+
puts " #{label}"
|
|
59
|
+
puts " Query : #{query.inspect}"
|
|
60
|
+
puts " Match : #{names.inspect}"
|
|
60
61
|
puts
|
|
61
62
|
end
|
|
62
63
|
|
|
@@ -91,19 +92,22 @@ puts " High threshold : returns all (#{result.size} servers) — no match abov
|
|
|
91
92
|
|
|
92
93
|
puts
|
|
93
94
|
section "mcp_discovery: true on a Robot"
|
|
94
|
-
|
|
95
|
-
RobotLab.build(
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
show_code <<~RUBY
|
|
96
|
+
robot = RobotLab.build(
|
|
97
|
+
name: "assistant",
|
|
98
|
+
provider: "ollama",
|
|
99
|
+
model: "qwen3.6:latest",
|
|
98
100
|
mcp_discovery: true,
|
|
99
101
|
mcp: [
|
|
100
|
-
{ name: "filesystem", description: "Read, write...",
|
|
101
|
-
{ name: "github", description: "GitHub repos...",
|
|
102
|
-
{ name: "brew", description: "Install packages...",
|
|
102
|
+
{ name: "filesystem", description: "Read, write...", transport: { } },
|
|
103
|
+
{ name: "github", description: "GitHub repos...", transport: { } },
|
|
104
|
+
{ name: "brew", description: "Install packages...", transport: { } }
|
|
103
105
|
]
|
|
104
106
|
)
|
|
105
107
|
|
|
106
|
-
# Only the :brew server is connected for this message
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
# Only the :brew server is connected for this message.
|
|
109
|
+
# mcp: :inherit and tools: :inherit are both required — run() defaults
|
|
110
|
+
# both to :none, which connects no servers and sends no tools.
|
|
111
|
+
robot.run("install imagemagick", mcp: :inherit, tools: :inherit)
|
|
112
|
+
RUBY
|
|
109
113
|
hr
|
data/examples/29_ractor_tools.rb
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
# - ToolError — propagated when a tool raises inside a Ractor
|
|
17
17
|
# - Parallel batch — many threads submitting concurrently vs sequentially
|
|
18
18
|
#
|
|
19
|
-
# Usage (no LLM
|
|
19
|
+
# Usage (no LLM traffic — pure CPU work):
|
|
20
20
|
# bundle exec ruby examples/29_ractor_tools.rb
|
|
21
21
|
#
|
|
22
22
|
# Good to know:
|
|
@@ -157,7 +157,9 @@ puts " Inner :tags array frozen? #{frozen[:tags].frozen?}"
|
|
|
157
157
|
puts " Inner :meta hash frozen? #{frozen[:meta].frozen?}"
|
|
158
158
|
puts
|
|
159
159
|
|
|
160
|
-
#
|
|
160
|
+
# Non-shareable objects (IO handles, Procs, ...) cannot cross a Ractor
|
|
161
|
+
# boundary — freeze_deep rejects them immediately rather than failing later
|
|
162
|
+
# inside a worker.
|
|
161
163
|
require "stringio"
|
|
162
164
|
begin
|
|
163
165
|
RobotLab::RactorBoundary.freeze_deep(StringIO.new("I cannot be frozen"))
|
|
@@ -15,20 +15,20 @@
|
|
|
15
15
|
# - RactorNetworkScheduler — direct use for testing / custom wiring
|
|
16
16
|
# - Result Hash — run_pipeline returns { name => result }
|
|
17
17
|
#
|
|
18
|
-
# Part 1 — simulated scheduler, no
|
|
18
|
+
# Part 1 — simulated scheduler, no LLM traffic.
|
|
19
19
|
# Overrides execute_spec with a sleep-based stub so the wave ordering and
|
|
20
20
|
# timing characteristics are visible without real LLM calls.
|
|
21
21
|
#
|
|
22
22
|
# Part 2 — Network.new(parallel_mode: :ractor) API walkthrough.
|
|
23
23
|
# Shows how to configure the network and inspects the dependency graph.
|
|
24
|
-
# No run() call is made, so no
|
|
24
|
+
# No run() call is made, so no LLM traffic either.
|
|
25
25
|
#
|
|
26
|
-
# Part 3 — live LLM run (
|
|
27
|
-
#
|
|
26
|
+
# Part 3 — live LLM run (opt-in via RUN_LIVE).
|
|
27
|
+
# Expected to fail against ruby_llm today; the rescue clauses explain why.
|
|
28
28
|
#
|
|
29
29
|
# Usage:
|
|
30
|
-
# bundle exec ruby examples/30_ractor_network.rb
|
|
31
|
-
#
|
|
30
|
+
# bundle exec ruby examples/30_ractor_network.rb # Parts 1 & 2
|
|
31
|
+
# RUN_LIVE=1 bundle exec ruby examples/30_ractor_network.rb # Parts 1, 2 & 3
|
|
32
32
|
|
|
33
33
|
require_relative "common"
|
|
34
34
|
require "robot_lab/ractor"
|
|
@@ -69,7 +69,7 @@ LATENCIES = {
|
|
|
69
69
|
#
|
|
70
70
|
# Speedup ≈ 1.7×
|
|
71
71
|
|
|
72
|
-
section "Part 1: Simulated Parallel Run (no
|
|
72
|
+
section "Part 1: Simulated Parallel Run (no LLM traffic)"
|
|
73
73
|
|
|
74
74
|
# SimulatedScheduler overrides execute_spec so that instead of
|
|
75
75
|
# constructing a real Robot and calling the LLM, it just sleeps for
|
|
@@ -141,27 +141,25 @@ section "Part 2: Network.new(parallel_mode: :ractor)"
|
|
|
141
141
|
# routes through RactorNetworkScheduler instead of SimpleFlow::Pipeline.
|
|
142
142
|
# The default mode is :async (unchanged SimpleFlow behavior).
|
|
143
143
|
|
|
144
|
-
model = LLM[:default].model
|
|
145
|
-
|
|
146
144
|
network = RobotLab::Network.new(name: "research_pipeline", parallel_mode: :ractor) do
|
|
147
145
|
task :headline_finder, RobotLab.build(name: "headline_finder",
|
|
148
146
|
system_prompt: "Find the 3 most relevant news headlines. Be concise.",
|
|
149
|
-
|
|
147
|
+
**llm_opts),
|
|
150
148
|
depends_on: :none
|
|
151
149
|
|
|
152
150
|
task :background_brief, RobotLab.build(name: "background_brief",
|
|
153
151
|
system_prompt: "Provide a 2-sentence background on the topic.",
|
|
154
|
-
|
|
152
|
+
**llm_opts),
|
|
155
153
|
depends_on: :none
|
|
156
154
|
|
|
157
155
|
task :fact_checker, RobotLab.build(name: "fact_checker",
|
|
158
156
|
system_prompt: "List 3 verifiable facts about the topic.",
|
|
159
|
-
|
|
157
|
+
**llm_opts),
|
|
160
158
|
depends_on: :none
|
|
161
159
|
|
|
162
160
|
task :report_writer, RobotLab.build(name: "report_writer",
|
|
163
161
|
system_prompt: "Synthesize the provided context into a 3-sentence report.",
|
|
164
|
-
|
|
162
|
+
**llm_opts),
|
|
165
163
|
depends_on: ["headline_finder", "background_brief", "fact_checker"]
|
|
166
164
|
end
|
|
167
165
|
|
|
@@ -187,21 +185,28 @@ puts " • Return value is a Hash { robot_name => result_string }"
|
|
|
187
185
|
puts
|
|
188
186
|
|
|
189
187
|
# =============================================================================
|
|
190
|
-
# Part 3: Live LLM run (
|
|
188
|
+
# Part 3: Live LLM run (opt-in via RUN_LIVE=1)
|
|
191
189
|
# =============================================================================
|
|
192
190
|
|
|
193
|
-
unless ENV["
|
|
191
|
+
unless ENV["RUN_LIVE"]
|
|
194
192
|
section "Part 3: Live LLM Run"
|
|
195
|
-
puts " Set
|
|
193
|
+
puts " Set RUN_LIVE=1 (with Ollama running) to attempt the real pipeline."
|
|
196
194
|
puts " Expected behavior: headline_finder, background_brief, and"
|
|
197
195
|
puts " fact_checker run in parallel; report_writer follows."
|
|
198
196
|
puts
|
|
197
|
+
puts " Heads-up: this is expected to fail today — see the rescue clauses"
|
|
198
|
+
puts " below. ruby_llm keeps Procs in class-level state, which cannot"
|
|
199
|
+
puts " cross a Ractor boundary. Part 1 shows the wave ordering that Part 3"
|
|
200
|
+
puts " will exercise once ruby_llm is Ractor-safe."
|
|
201
|
+
puts
|
|
199
202
|
hr
|
|
200
203
|
puts "Example 30 complete."
|
|
201
204
|
exit 0
|
|
202
205
|
end
|
|
203
206
|
|
|
204
|
-
|
|
207
|
+
require_ollama!
|
|
208
|
+
|
|
209
|
+
section "Part 3: Live LLM Run (RUN_LIVE set)"
|
|
205
210
|
|
|
206
211
|
puts " Running 4-robot research pipeline on:"
|
|
207
212
|
puts " \"#{topic}\""
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
# RunConfig.new(max_concurrent_robots: 4)
|
|
46
46
|
#
|
|
47
47
|
# Usage:
|
|
48
|
-
#
|
|
48
|
+
# ruby examples/31_launch_assessment.rb
|
|
49
49
|
|
|
50
50
|
require_relative "common"
|
|
51
51
|
|
|
@@ -61,7 +61,7 @@ class AnalystRobot < RobotLab::Robot
|
|
|
61
61
|
def initialize(name:, memory_key:, role:)
|
|
62
62
|
super(
|
|
63
63
|
name: name,
|
|
64
|
-
|
|
64
|
+
**llm_opts,
|
|
65
65
|
system_prompt: "You are a #{role}. " \
|
|
66
66
|
"Review the product brief in 2-3 crisp sentences from your area of expertise. " \
|
|
67
67
|
"Close with a one-word verdict: READY or NOT-READY."
|
|
@@ -111,12 +111,7 @@ class LaunchDirector < RobotLab::Robot
|
|
|
111
111
|
|
|
112
112
|
def call(result)
|
|
113
113
|
puts " [#{name}] reading all findings from shared memory..."
|
|
114
|
-
findings =
|
|
115
|
-
|
|
116
|
-
if findings.values.any? { |v| v == :timeout }
|
|
117
|
-
timed_out = findings.select { |_, v| v == :timeout }.keys
|
|
118
|
-
puts " [#{name}] WARNING: timed out waiting for: #{timed_out.join(", ")}"
|
|
119
|
-
end
|
|
114
|
+
findings = collect_findings
|
|
120
115
|
|
|
121
116
|
prompt = <<~PROMPT
|
|
122
117
|
Six specialist analysts have reviewed our product launch readiness.
|
|
@@ -139,6 +134,22 @@ class LaunchDirector < RobotLab::Robot
|
|
|
139
134
|
|
|
140
135
|
result.with_context(:recommendation, recommendation).continue(recommendation)
|
|
141
136
|
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
# Memory#get(wait:) RAISES RobotLab::AwaitTimeout on expiry — it never
|
|
141
|
+
# returns a :timeout sentinel. Rescue it and re-read without wait:, which
|
|
142
|
+
# never blocks and omits keys that were never written, so a stalled analyst
|
|
143
|
+
# degrades the recommendation instead of killing the director.
|
|
144
|
+
def collect_findings
|
|
145
|
+
@shared_memory.get(*FINDING_KEYS, wait: 120)
|
|
146
|
+
rescue RobotLab::AwaitTimeout => e
|
|
147
|
+
puts " [#{name}] WARNING: #{e.message}"
|
|
148
|
+
partial = @shared_memory.get(*FINDING_KEYS)
|
|
149
|
+
missing = FINDING_KEYS - partial.keys
|
|
150
|
+
puts " [#{name}] Proceeding without: #{missing.join(', ')}" if missing.any?
|
|
151
|
+
partial
|
|
152
|
+
end
|
|
142
153
|
end
|
|
143
154
|
|
|
144
155
|
# ── Product Brief ───────────────────────────────────────────────────────────────
|
|
@@ -169,7 +180,7 @@ end
|
|
|
169
180
|
|
|
170
181
|
director = LaunchDirector.new(
|
|
171
182
|
name: "launch_director",
|
|
172
|
-
|
|
183
|
+
**llm_opts,
|
|
173
184
|
system_prompt: "You are the VP of Product making the final launch call."
|
|
174
185
|
)
|
|
175
186
|
|
|
@@ -3,8 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
# Example 32: Newsletter Issue Retriever
|
|
5
5
|
#
|
|
6
|
+
# NOTE: this is a plain utility script, not a RobotLab demo — it uses no part
|
|
7
|
+
# of the library. It lives here as the content feeder for other experiments.
|
|
8
|
+
# Read examples 01-31 for library capabilities.
|
|
9
|
+
#
|
|
6
10
|
# Fetches all unprocessed issues from multiple Ruby newsletters via RSS
|
|
7
|
-
# and saves each as a Markdown file in
|
|
11
|
+
# and saves each as a Markdown file in a Markdown clippings folder.
|
|
8
12
|
#
|
|
9
13
|
# - Processes oldest unprocessed issue first (across all feeds)
|
|
10
14
|
# - Filename: <newsletter-name>_YYYYMMDD.md
|
|
@@ -13,6 +17,7 @@
|
|
|
13
17
|
#
|
|
14
18
|
# Usage:
|
|
15
19
|
# ruby examples/32_newsletter_reader.rb
|
|
20
|
+
# CLIPPINGS_DIR=~/Notes/Clippings ruby examples/32_newsletter_reader.rb
|
|
16
21
|
|
|
17
22
|
require "net/http"
|
|
18
23
|
require "open3"
|
|
@@ -28,7 +33,7 @@ NEWSLETTER_RSS_URLS = [
|
|
|
28
33
|
"https://cprss.s3.amazonaws.com/rubyweekly.com.xml" # Ruby Weekly
|
|
29
34
|
].freeze
|
|
30
35
|
|
|
31
|
-
CLIPPINGS_DIR = File.expand_path("
|
|
36
|
+
CLIPPINGS_DIR = File.expand_path(ENV.fetch("CLIPPINGS_DIR", "~/Documents/Clippings"))
|
|
32
37
|
PROCESSED_STATE_FILE = File.join(Dir.home, ".robot_lab", "newsletter_processed.yaml")
|
|
33
38
|
|
|
34
39
|
# Tracks which newsletter issue URLs have been saved.
|
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
# Prerequisites:
|
|
15
15
|
# gem install redis
|
|
16
16
|
# Redis server running on localhost:6379
|
|
17
|
+
# Ollama running with the model from common.rb pulled
|
|
17
18
|
#
|
|
18
19
|
# Usage:
|
|
19
20
|
# ruby examples/33_stock_predictor.rb
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
require "robot_lab"
|
|
22
|
+
require_relative "common"
|
|
23
23
|
require "robot_lab/durable"
|
|
24
24
|
require "redis"
|
|
25
25
|
require "json"
|
|
@@ -204,12 +204,15 @@ puts "Warmup : #{PredictorConfig.sma_window} ticks"
|
|
|
204
204
|
puts "Press Ctrl-C to stop."
|
|
205
205
|
puts "-" * 60
|
|
206
206
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
207
|
+
require_ollama!
|
|
208
|
+
|
|
209
|
+
redis = Redis.new
|
|
210
|
+
prices = []
|
|
211
|
+
|
|
212
|
+
robot = RobotLab.build(
|
|
213
|
+
**llm_opts,
|
|
214
|
+
name: "predictor_tuner",
|
|
215
|
+
system_prompt: <<~PROMPT,
|
|
213
216
|
You are a quantitative analyst tuning an ensemble stock price range
|
|
214
217
|
predictor for ticker XYZZY. Each prediction covers the high and low
|
|
215
218
|
price over the next #{WINDOW_SIZE} ticks.
|
|
@@ -231,10 +234,21 @@ robot = RobotLab.build(
|
|
|
231
234
|
4. If you observe a reliable pattern, call RecordKnowledge to preserve it.
|
|
232
235
|
5. When uncertain, do nothing rather than guess.
|
|
233
236
|
PROMPT
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
237
|
+
local_tools: [AdjustParameters]
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Durable learning is enabled AFTER construction. Robot#initialize takes a
|
|
241
|
+
# fixed keyword list with no **rest, so `learn:`/`learn_domain:` constructor
|
|
242
|
+
# params raise ArgumentError; the Durable::Learning mixin exposes
|
|
243
|
+
# setup_durable_learning instead. It seeds @learnings from
|
|
244
|
+
# ~/.robot_lab/durable/<domain>.yml and appends the RecallKnowledge and
|
|
245
|
+
# RecordKnowledge tools to local_tools.
|
|
246
|
+
robot.setup_durable_learning(domain: "xyzzy stock prediction")
|
|
247
|
+
|
|
248
|
+
puts "Durable domain : xyzzy stock prediction"
|
|
249
|
+
puts "Tools : #{robot.local_tools.map(&:name).join(', ')}"
|
|
250
|
+
puts "Seeded learnings: #{robot.learnings.size}"
|
|
251
|
+
puts "-" * 60
|
|
238
252
|
|
|
239
253
|
warmed_up = false
|
|
240
254
|
pending_pred = nil # { prediction: {high:, low:}, window_prices: [] }
|
|
@@ -291,10 +305,17 @@ redis.subscribe(CHANNEL) do |on|
|
|
|
291
305
|
puts "#{"─" * 60}"
|
|
292
306
|
|
|
293
307
|
print " [tuner] analyzing window #{window_num}..."
|
|
294
|
-
|
|
308
|
+
# tools: :inherit is required — run() defaults to :none, which sends the
|
|
309
|
+
# provider an empty tool list, so AdjustParameters/RecallKnowledge/
|
|
310
|
+
# RecordKnowledge would never be callable.
|
|
311
|
+
tuner_response = robot.run(tuner_prompt(result), tools: :inherit)
|
|
295
312
|
tuner_line = tuner_response.reply.lines.first&.chomp || "(no response)"
|
|
296
313
|
puts "\r [tuner] #{tuner_line}#{" " * 20}"
|
|
297
314
|
puts " Params: #{PredictorConfig.summary}"
|
|
315
|
+
|
|
316
|
+
# Promote this session's learnings into the durable YAML store. Core
|
|
317
|
+
# Robot#run does not invoke the reflector, so the caller drives it.
|
|
318
|
+
robot.run_reflector
|
|
298
319
|
puts
|
|
299
320
|
|
|
300
321
|
# ── Start next window ──────────────────────────────────────────
|
data/examples/34_agentskills.rb
CHANGED
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
# Usage:
|
|
11
11
|
# mkdir -p ~/.prompts/skills/code_reviewer
|
|
12
12
|
# # (create SKILL.md as shown in the example header)
|
|
13
|
-
#
|
|
13
|
+
# ruby examples/34_agentskills.rb
|
|
14
14
|
|
|
15
15
|
require_relative "common"
|
|
16
|
+
require "robot_lab/document_store"
|
|
16
17
|
|
|
17
18
|
require "logger"
|
|
18
19
|
log_file = File.join(__dir__, "34.log")
|
|
@@ -36,13 +37,16 @@ end
|
|
|
36
37
|
# "Review Ruby code for quality, style, and potential bugs",
|
|
37
38
|
# the skill's instructions are injected into the system prompt.
|
|
38
39
|
robot = RobotLab.build(
|
|
39
|
-
|
|
40
|
+
**llm_opts,
|
|
40
41
|
name: "assistant",
|
|
41
42
|
system_prompt: "You are a helpful Ruby programming assistant.",
|
|
42
43
|
skills: [:code_reviewer]
|
|
43
44
|
)
|
|
44
45
|
|
|
45
|
-
puts "
|
|
46
|
+
puts "Declared skills: #{robot.skills.inspect}"
|
|
47
|
+
puts "(Folder-format skills are matched per-run by embedding similarity, so"
|
|
48
|
+
puts " whether one is injected depends on the message — watch the two queries"
|
|
49
|
+
puts " below.)"
|
|
46
50
|
puts
|
|
47
51
|
|
|
48
52
|
# Message semantically related to code review — skill should activate
|