robot_lab-ractor 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 24693d3b0371d41a878e7112c904e01bbf65d46f4cded26fe78b45856c49716c
4
- data.tar.gz: e7fdf7b457efb4e5359f9ccf36afd50823f2fe15d11e5c56d6dd2fe93ba9f959
3
+ metadata.gz: 7df9c3ab4660c44f064946a5a90f071df1ad03baa1b4cac8c3259156a35be70b
4
+ data.tar.gz: 0fb20202b60255f45be1b423d5fa5870730cfd4fbf3dbaee1ec601eb72a87f59
5
5
  SHA512:
6
- metadata.gz: 5ed65c66df3ef0d840ab8f4745cf2a3b2fe82c6216f0458480336ec3274fafde8ce8b2c610526de66f00cdf9473ab52961b6f818166b64752a911a377cb21a34
7
- data.tar.gz: 9024af8c7f52c239b8549e04eab3c9f852622264a3f3656aac4d41cfc72ccb41707b07ec7749622aaddb4c8ff95d21af0c7e0aa106bedf25c4a58d7de15e9095
6
+ metadata.gz: '0418c60678b46d1c7b9e3a625f8e2c3b1ebf789a2f526daea6f05d5c8a9046a11e54217bea1dd8fd15a50bae6115ea634907e002adc96d7d06f736c626f05d20'
7
+ data.tar.gz: c8800eee270d7987d17658a97c7dce82074ce8db09b67d7a598d92c1e29b15138d07e242f4a5de86fe0a812cadbb10c0ce02736da6d80f60cea36f24b22b4779
data/.envrc CHANGED
@@ -1 +1,3 @@
1
+ source_up
1
2
  export RR=`pwd`
3
+ export BUNDLE_GEMFILE=Gemfile
data/.loki ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+ # robot_lab-ractor — CPU parallelism via Ractors.
3
+
4
+ import_up "repo_dev.loki"
5
+
6
+ class Tasks
7
+ @@gem_name ||= "robot_lab-ractor".freeze
8
+
9
+ header "robot_lab-ractor v#{gem_version} — CPU-parallel robot execution"
10
+ end
@@ -0,0 +1,3 @@
1
+ 1 lib/robot_lab/ractor_boundary.rb
2
+ 3 lib/robot_lab/ractor_network_scheduler.rb
3
+ 1 lib/robot_lab/ractor_worker_pool.rb
data/.rubocop.yml ADDED
@@ -0,0 +1 @@
1
+ inherit_from: ../.rubocop-base.yml
data/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ### Added
4
+ - `.loki` Asgard task file: `test`, `rubocop`, `rubocop_fix`, `flog`, `flay`, `quality`, `build`, `install`, `release`, and `console` tasks via the Asgard task runner
5
+ - `flay_check` Rake task: structural code duplication gate (mass threshold 50); integrated into the `quality` Rake task
6
+ - `flay` and `minitest-reporters` gems added to development dependencies
7
+ - `test_output.txt`, `flay_output.txt`, `flog_output.txt`, and `rubocop_output.txt` added to `.gitignore`
8
+
9
+ ### Changed
10
+ - `test/test_helper.rb`: test output redirected to `test_output.txt` via `$stdout` reassignment; `TerminalSummaryReporter` prints a single PASS/FAIL summary line to the terminal
11
+ - `Rakefile`: `rubocop` and `rubocop_fix` tasks removed (now owned by Asgard); `flay_check` integrated into the `quality` gate
12
+
13
+ ## [0.2.1] - 2026-05-19
14
+
15
+ ### Added
16
+ - `RactorWorkerPool` — shared pool of Ractor workers; tools marked `ractor_safe true` are automatically routed through it instead of running inline
17
+ - `RactorNetworkScheduler` — DAG-aware parallel execution of robot networks using Ractors; activated via `parallel_mode: :ractor` on a network
18
+ - `RactorBoundary` — `freeze_deep` utility for making values Ractor-shareable; raises `RactorBoundaryError` for values that cannot cross Ractor boundaries
19
+ - `RactorMemoryProxy` — thread-safe proxy exposing `RobotLab::Memory` to Ractor workers via `Ractor::Wrapper`
20
+ - `RactorJob` — Ractor-shareable frozen job value object used internally by the pool and scheduler
21
+ - `ractor_safe` class-level DSL for `RubyLLM::Tool` / `RobotLab::Tool` subclasses; inherited by subclasses
22
+ - `RobotLab.ractor_pool` / `.shutdown_ractor_pool` — process-level pool lifecycle methods added to the `RobotLab` module
23
+ - Ractor Parallelism guide (`docs/guides/ractor-parallelism.md`) covering both CPU-bound tools and parallel network pipelines
24
+
25
+ ### Changed
26
+ - Version synchronized with robot_lab core 0.2.1
27
+
3
28
  ## [0.1.0] - 2026-05-07
4
29
 
5
30
  - Initial release
data/CLAUDE.md ADDED
@@ -0,0 +1,71 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What This Gem Does
6
+
7
+ `robot_lab-ractor` enables CPU-parallel execution in RobotLab via Ruby Ractors. It provides a worker pool for CPU-bound tools and a dependency-aware network scheduler for running robot pipelines across Ractor workers.
8
+
9
+ ## Commands
10
+
11
+ ```bash
12
+ bundle exec rake test # Run full test suite
13
+ ruby -Ilib:test test/<file> # Run a single test file
14
+ ```
15
+
16
+ ## CRITICAL: Never define `module RobotLab::Ractor`
17
+
18
+ `lib/robot_lab/ractor.rb` intentionally does **not** open `module RobotLab::Ractor`. Defining that module would shadow Ruby's built-in `Ractor` class for all code inside the `RobotLab` namespace, breaking `Ractor.new`, `Ractor.make_shareable`, etc. throughout the codebase. The version constant lives in `RobotLab::RactorPool::VERSION` to avoid this clash.
19
+
20
+ ## Architecture
21
+
22
+ **`RactorWorkerPool`** (`ractor_worker_pool.rb`) — Pool of Ractor workers for CPU-bound, Ractor-safe tools. Submit a job from any thread; the result is returned synchronously via a per-job `RactorQueue`.
23
+
24
+ ```ruby
25
+ pool = RactorWorkerPool.new(size: 4) # :auto = Etc.nprocessors
26
+ result = pool.submit("MyTool", { arg: "value" })
27
+ pool.shutdown
28
+ ```
29
+
30
+ Accessed globally via `RobotLab.ractor_pool` / `RobotLab.shutdown_ractor_pool`.
31
+
32
+ **`RactorNetworkScheduler`** (`ractor_network_scheduler.rb`) — Schedules frozen `RobotSpec` payloads across Ractor workers in dependency order. Entry-point robots (`:none` deps) dispatch immediately; dependent robots dispatch once all named deps complete.
33
+
34
+ ```ruby
35
+ scheduler = RactorNetworkScheduler.new(memory: shared_memory)
36
+ results = scheduler.run_pipeline([
37
+ { spec: analyst_spec, depends_on: :none },
38
+ { spec: writer_spec, depends_on: ["analyst"] }
39
+ ], message: "Analyse this")
40
+ scheduler.shutdown
41
+ ```
42
+
43
+ **`RactorBoundary`** (`ractor_boundary.rb`) — Utility for crossing Ractor boundaries. `freeze_deep(obj)` recursively freezes Hash/Array structures and calls `Ractor.make_shareable`. Raises `RactorBoundaryError` if the value cannot be made shareable (e.g. a live IO or Proc).
44
+
45
+ **`RactorMemoryProxy`** (`ractor_memory_proxy.rb`) — Wraps a `Memory` instance via `Ractor::Wrapper` so Ractor workers can read and write shared state. Only `get`, `set`, and `keys` are proxied; closures and subscriptions are not Ractor-safe and must use the thread-side Memory directly.
46
+
47
+ **Data Carriers** (`ractor_job.rb`) — Three frozen `Data.define` structs:
48
+ - `RactorJob` — work item: `id`, `type`, `payload`, `reply_queue`
49
+ - `RactorJobError` — serialised error: `message`, `backtrace`
50
+ - `RobotSpec` — frozen robot descriptor: `name`, `template`, `system_prompt`, `config_hash`, `hook_classes`
51
+
52
+ ## Tool Requirements for Ractor Safety
53
+
54
+ Tools submitted to `RactorWorkerPool` must:
55
+ 1. Be instantiated fresh per call (no shared mutable state)
56
+ 2. Accept only Ractor-shareable arguments (frozen strings, numbers, frozen hashes)
57
+ 3. Return a value that can be `Ractor.make_shareable` (frozen or deep-freezable)
58
+
59
+ LLM calls via `ruby_llm` are NOT Ractor-safe — do not submit tools that call the LLM to the pool.
60
+
61
+ ## Key Constraints
62
+
63
+ - Shutdown is via poison-pill (one `nil` per worker) — always call `shutdown` to avoid orphaned Ractors.
64
+ - `RactorNetworkScheduler.process_job` and `RactorWorkerPool.process_job` must be class methods — Ractors cannot call instance methods on objects defined outside their scope.
65
+ - `RobotSpec#hook_classes` must be an array of frozen class references, not instances.
66
+
67
+ ## Testing
68
+
69
+ - Minitest with SimpleCov (branch coverage tracked, no minimum threshold enforced yet)
70
+ - Tests use RobotLab stubs (the full `robot_lab` gem is NOT loaded in tests — see `test_helper.rb`)
71
+ - Coverage baseline: ~55% line / ~32% branch
data/Rakefile CHANGED
@@ -1,8 +1,132 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler/gem_tasks"
4
- require "minitest/test_task"
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
5
 
6
- Minitest::TestTask.create
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << 'test'
8
+ t.libs << 'lib'
9
+ t.test_files = FileList['test/**/*_test.rb', 'test/**/test_*.rb'].exclude('**/*_helper.rb')
10
+ t.verbose = true
11
+ t.ruby_opts << '-rtest_helper'
12
+ end
7
13
 
8
14
  task default: :test
15
+
16
+ desc 'Run tests with verbose output'
17
+ task :test_verbose do
18
+ ENV['TESTOPTS'] = '--verbose'
19
+ Rake::Task[:test].invoke
20
+ end
21
+
22
+ desc 'Run a single test file'
23
+ task :test_file, [:file] do |_t, args|
24
+ ruby "test/#{args[:file]}"
25
+ end
26
+
27
+ desc 'Check code complexity with Flog (warn >=20, fail >=50)'
28
+ task :flog_check do
29
+ require 'flog'
30
+
31
+ method_warn = 20.0
32
+ method_fail = 50.0
33
+
34
+ flogger = Flog.new(all: true)
35
+ flogger.flog(*Dir.glob('lib/**/*.rb'))
36
+
37
+ warnings = []
38
+ failures = []
39
+
40
+ flogger.each_by_score do |method, score|
41
+ next if method.end_with?('#none')
42
+
43
+ if score > method_fail
44
+ failures << "#{format('%.1f', score)}: #{method}"
45
+ elsif score > method_warn
46
+ warnings << "#{format('%.1f', score)}: #{method}"
47
+ end
48
+ end
49
+
50
+ unless warnings.empty?
51
+ puts "\nFlog warnings (#{method_warn}–#{method_fail}) — target for future refactoring:"
52
+ warnings.each { |v| puts " #{v}" }
53
+ end
54
+
55
+ if failures.empty?
56
+ puts "\nFlog: no methods exceed the failure threshold (>=#{method_fail})"
57
+ else
58
+ puts "\nFlog failures (>=#{method_fail}) — must be refactored:"
59
+ failures.each { |v| puts " #{v}" }
60
+ abort "\nFlog quality gate failed: #{failures.size} method(s) exceed #{method_fail}"
61
+ end
62
+ end
63
+
64
+ desc 'Check for structural code duplication with Flay (mass >= 50)'
65
+ task :flay_check do
66
+ require 'flay'
67
+
68
+ mass_threshold = 50
69
+
70
+ flay = Flay.new({ mass: mass_threshold, diff: false, verbose: false, summary: false, timeout: 60 })
71
+ flay.process(*Dir.glob('lib/**/*.rb'))
72
+ flay.analyze
73
+
74
+ if flay.hashes.empty?
75
+ puts "\nFlay: no structural duplication detected (mass >= #{mass_threshold})"
76
+ else
77
+ puts "\nFlay found structural duplication (mass >= #{mass_threshold}):"
78
+ flay.report
79
+ abort "\nFlay quality gate failed: #{flay.hashes.length} pattern(s) detected"
80
+ end
81
+ end
82
+
83
+ desc 'Run all quality checks: tests (with coverage), RuboCop, Flog, and Flay'
84
+ task :quality do
85
+ gates = [
86
+ ['Tests + Coverage', 'bundle exec rake test'],
87
+ ['RuboCop', 'bundle exec rubocop'],
88
+ ['Flog Complexity', 'bundle exec rake flog_check'],
89
+ ['Flay Duplication', 'bundle exec rake flay_check']
90
+ ]
91
+
92
+ results = gates.map do |label, command|
93
+ puts "\n#{'=' * 60}"
94
+ puts "Quality Gate: #{label}"
95
+ puts '=' * 60
96
+ [label, system(command) ? :pass : :fail]
97
+ end
98
+
99
+ green = ->(s) { "\e[32m#{s}\e[0m" }
100
+ red = ->(s) { "\e[31m#{s}\e[0m" }
101
+ width = results.map { |label, _| label.length }.max
102
+
103
+ puts "\n#{'=' * 60}"
104
+ puts 'Quality Gate Summary'
105
+ puts '=' * 60
106
+ results.each do |label, status|
107
+ badge = status == :pass ? green.call('PASS') : red.call('FAIL')
108
+ puts " [#{badge}] #{label.ljust(width)}"
109
+ end
110
+ puts '-' * 60
111
+
112
+ passed = results.count { |_, s| s == :pass }
113
+ failed = results.count { |_, s| s == :fail }
114
+ tally = "#{passed} passed, #{failed} failed"
115
+ puts " #{failed.zero? ? green.call(tally) : red.call(tally)}"
116
+ puts '=' * 60
117
+
118
+ abort "\n#{red.call('Quality gate failed.')}" unless failed.zero?
119
+ puts "\n#{green.call('All quality gates passed.')}"
120
+ end
121
+
122
+ namespace :docs do
123
+ desc 'Build MkDocs documentation'
124
+ task :build do
125
+ sh 'mkdocs build'
126
+ end
127
+
128
+ desc 'Serve MkDocs documentation locally on http://localhost:8000'
129
+ task :serve do
130
+ sh 'mkdocs serve'
131
+ end
132
+ end
@@ -229,6 +229,71 @@ Values written via `set` are automatically deep-frozen before crossing the bound
229
229
 
230
230
  ---
231
231
 
232
+ ## Hooks in Ractor Workers
233
+
234
+ `RactorNetworkScheduler` carries a robot's registered `RobotLab::Hook` handler
235
+ classes across the Ractor boundary so the full hook pipeline still fires
236
+ inside each worker.
237
+
238
+ When the scheduler builds a `RobotSpec`, it collects handler classes from all
239
+ three registration levels and stores them in the frozen `hook_classes` field:
240
+
241
+ ```ruby
242
+ def ractor_hook_classes_for(robot)
243
+ [RobotLab.hooks, @hooks, robot.hooks]
244
+ .flat_map { |r| r.registrations.map(&:handler_class) }
245
+ .uniq.freeze
246
+ end
247
+ ```
248
+
249
+ | Registration level | Collected by |
250
+ |---|---|
251
+ | `RobotLab.on(Hook)` | `RobotLab.hooks` (global) |
252
+ | `network.on(Hook)` | `@hooks` (network) |
253
+ | `robot.on(Hook)` | `robot.hooks` (robot) |
254
+
255
+ Inside each worker, the scheduler re-registers the handlers on the freshly
256
+ built robot before running it:
257
+
258
+ ```ruby
259
+ spec.hook_classes.each { |handler| robot.on(handler) }
260
+ robot.run(message) # full hook pipeline fires here
261
+ ```
262
+
263
+ This works because a Hook handler is a Ruby **class**, and classes are
264
+ Ractor-shareable constants — they cross the boundary without any
265
+ serialization.
266
+
267
+ ### Writing a Ractor-Safe Hook
268
+
269
+ A hook handler that may run inside a Ractor worker must follow the same
270
+ statelessness rules as a Ractor-safe tool:
271
+
272
+ - **No class-level state.** `@ivars` set on the class object live in the main
273
+ Ractor; non-main Ractors cannot read or write them, even when the values
274
+ themselves would be shareable.
275
+ - **Accumulate events on the context, not the class.** Push results into
276
+ `ctx.local` (a plain object local to that Ractor's run) and return them as
277
+ part of the worker's result tuple so the main thread can aggregate them
278
+ after `Ractor#value`.
279
+ - **Avoid `$stdout.puts` for anything you need interleaved with main-thread
280
+ output.** Output from non-main Ractors is buffered per-Ractor and only
281
+ flushes at Ractor exit. `Kernel#warn` is silently dropped. Reading the
282
+ `STDOUT` constant raises `IsolationError`. `$stderr.puts` and a
283
+ `File.open(frozen_path, "a")` opened fresh inside the worker are both safe.
284
+ - **`on_error` fires inside the failing Ractor**, before the error propagates
285
+ back to the main thread. Surface whatever you need via the return tuple —
286
+ the main thread only learns of the failure when it calls `worker.value`.
287
+
288
+ `examples/03_ractor_hooks.rb` walks through all of this with two example
289
+ handlers (`TraceHook`, `PerfHook`) plus `RobotLab::Xyzzy` — a fully
290
+ Ractor-safe tracer covering all five hook families (`run`, `llm_generation`,
291
+ `tool_call`, `network_run`, `task`) — run directly against the hook mechanic
292
+ without touching `ruby_llm` (which has class-level Proc callbacks that
293
+ prevent construction inside Ractors today).
294
+
295
+ ---
296
+
232
297
  ## The Frozen-Data Contract
233
298
 
234
299
  Everything that crosses a Ractor boundary must be Ractor-shareable: frozen strings, frozen hashes, frozen arrays, `Data.define` structs, and integers/symbols/nil.
@@ -356,6 +421,16 @@ at_exit { RobotLab.shutdown_ractor_pool }
356
421
 
357
422
  ---
358
423
 
424
+ ## Runnable Examples
425
+
426
+ - `examples/01_ractor_tools.rb` — CPU-bound tools routed through `RactorWorkerPool`
427
+ - `examples/02_ractor_network.rb` — a robot network run via `RactorNetworkScheduler`
428
+ - `examples/03_ractor_hooks.rb` — the hook system firing inside Ractor workers (no LLM API key required)
429
+
430
+ ```bash
431
+ bundle exec ruby examples/03_ractor_hooks.rb
432
+ ```
433
+
359
434
  ## Next Steps
360
435
 
361
436
  - [Using Tools](using-tools.md) — Tool definitions and configuration
data/docs/index.md CHANGED
@@ -60,6 +60,12 @@ pool = RobotLab.ractor_pool
60
60
  RobotLab.shutdown_ractor_pool
61
61
  ```
62
62
 
63
+ ## Examples
64
+
65
+ - `examples/01_ractor_tools.rb` — CPU-bound tools through `RactorWorkerPool`
66
+ - `examples/02_ractor_network.rb` — a robot network run via `RactorNetworkScheduler`
67
+ - `examples/03_ractor_hooks.rb` — the hook system firing inside Ractor workers (no LLM API key required)
68
+
63
69
  ## Links
64
70
 
65
71
  - [Ractor Parallelism Guide](guides/ractor-parallelism.md)
@@ -26,8 +26,7 @@
26
26
  # SHA-256 rounds (~320 ms on modern hardware) so the 4-6× speedup is
27
27
  # clearly visible on a 6-core machine.
28
28
 
29
- require "robot_lab"
30
- require "robot_lab/ractor"
29
+ require_relative "common"
31
30
  require "digest"
32
31
 
33
32
  # Always shut down the pool when the process exits.
@@ -114,12 +113,7 @@ end
114
113
  # Demo
115
114
  # =============================================================================
116
115
 
117
- puts "=" * 62
118
- puts "Example 29: Ractor-Safe CPU Tools"
119
- puts "=" * 62
120
- puts
121
-
122
- DIVIDER = ("─" * 54).freeze
116
+ banner("Ractor-Safe CPU Tools")
123
117
 
124
118
  SAMPLE_TEXTS = [
125
119
  "Ruby makes programmer happiness a first-class concern in language design.",
@@ -130,10 +124,7 @@ SAMPLE_TEXTS = [
130
124
  "Simplicity is the ultimate sophistication in software architecture."
131
125
  ].freeze
132
126
 
133
- # ── 1. ractor_safe? flags ─────────────────────────────────────
134
-
135
- puts "1. ractor_safe? flags"
136
- puts " #{DIVIDER}"
127
+ section("1. ractor_safe? flags")
137
128
 
138
129
  {
139
130
  "WordStatsTool" => WordStatsTool,
@@ -146,10 +137,7 @@ puts " #{DIVIDER}"
146
137
  end
147
138
  puts
148
139
 
149
- # ── 2. RactorBoundary.freeze_deep ─────────────────────────────
150
-
151
- puts "2. RactorBoundary.freeze_deep"
152
- puts " #{DIVIDER}"
140
+ section("2. RactorBoundary.freeze_deep")
153
141
 
154
142
  nested = { tags: ["ruby", "ractor"], meta: { version: 2 } }
155
143
  frozen = RobotLab::RactorBoundary.freeze_deep(nested)
@@ -170,11 +158,8 @@ rescue RobotLab::RactorBoundaryError => e
170
158
  end
171
159
  puts
172
160
 
173
- # ── 3. Single pool submissions ─────────────────────────────────
174
-
175
161
  pool = RobotLab.ractor_pool
176
- puts "3. Worker pool (#{pool.size} Ractors — one per CPU core)"
177
- puts " #{DIVIDER}"
162
+ section("3. Worker pool (#{pool.size} Ractors — one per CPU core)")
178
163
 
179
164
  sample = SAMPLE_TEXTS.first
180
165
 
@@ -185,10 +170,7 @@ r = pool.submit("ReadabilityTool", { text: sample })
185
170
  puts " ReadabilityTool: words_per_sentence=#{r[:words_per_sentence]} long_word_pct=#{r[:long_word_pct]}%"
186
171
  puts
187
172
 
188
- # ── 4. ToolError propagation ───────────────────────────────────
189
-
190
- puts "4. ToolError propagation"
191
- puts " #{DIVIDER}"
173
+ section("4. ToolError propagation")
192
174
  puts " Submitting nil as :text (WordStatsTool will call nil.scan — NoMethodError)"
193
175
  puts
194
176
 
@@ -200,10 +182,7 @@ rescue RobotLab::ToolError => e
200
182
  end
201
183
  puts
202
184
 
203
- # ── 5. Parallel batch vs sequential ───────────────────────────
204
-
205
- puts "5. Parallel batch — #{SAMPLE_TEXTS.length} jobs, each doing #{HeavyDigestTool::ROUNDS.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1_').reverse} SHA-256 rounds"
206
- puts " #{DIVIDER}"
185
+ section("5. Parallel batch — #{SAMPLE_TEXTS.length} jobs, #{HeavyDigestTool::ROUNDS.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1_').reverse} SHA-256 rounds each")
207
186
 
208
187
  # Parallel: one Thread per job, all submitted simultaneously.
209
188
  t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@@ -230,13 +209,8 @@ puts
230
209
  puts " First result (truncated digest): #{parallel_results.first}"
231
210
  puts
232
211
 
233
- # ── 6. Shutdown ────────────────────────────────────────────────
234
-
235
- puts "6. Shutdown"
236
- puts " #{DIVIDER}"
212
+ section("6. Shutdown")
237
213
  RobotLab.shutdown_ractor_pool
238
214
  puts " Pool shut down cleanly (poison-pill × #{pool.size} workers)."
239
215
  puts
240
- puts "=" * 62
241
- puts "Example 29 complete."
242
- puts "=" * 62
216
+ hr
@@ -30,15 +30,9 @@
30
30
  # bundle exec ruby examples/30_ractor_network.rb # Parts 1 & 2
31
31
  # ANTHROPIC_API_KEY=key ruby examples/30_ractor_network.rb # Parts 1, 2 & 3
32
32
 
33
- require "robot_lab"
34
- require "robot_lab/ractor"
33
+ require_relative "common"
35
34
 
36
- puts "=" * 62
37
- puts "Example 30: Ractor Network Scheduler"
38
- puts "=" * 62
39
- puts
40
-
41
- DIVIDER = ("─" * 54).freeze
35
+ banner("Ractor Network Scheduler")
42
36
 
43
37
  # =============================================================================
44
38
  # Part 1: Simulated scheduler — dependency waves and timing
@@ -72,8 +66,7 @@ LATENCIES = {
72
66
  #
73
67
  # Speedup ≈ 1.7×
74
68
 
75
- puts "── Part 1: Simulated parallel run (no API key) ───────────"
76
- puts
69
+ section("Part 1: Simulated parallel run (no API key)")
77
70
 
78
71
  # SimulatedScheduler overrides execute_spec so that instead of
79
72
  # constructing a real Robot and calling the LLM, it just sleeps for
@@ -139,8 +132,7 @@ puts
139
132
  # Part 2: Network.new(parallel_mode: :ractor) API
140
133
  # =============================================================================
141
134
 
142
- puts "── Part 2: Network.new(parallel_mode: :ractor) ───────────"
143
- puts
135
+ section("Part 2: Network.new(parallel_mode: :ractor)")
144
136
 
145
137
  # When parallel_mode: :ractor is set on a Network, network.run(message:)
146
138
  # routes through RactorNetworkScheduler instead of SimpleFlow::Pipeline.
@@ -196,19 +188,16 @@ puts
196
188
  # =============================================================================
197
189
 
198
190
  unless ENV["ANTHROPIC_API_KEY"]
199
- puts "── Part 3: Live LLM run ──────────────────────────────────"
191
+ section("Part 3: Live LLM run")
200
192
  puts " Set ANTHROPIC_API_KEY to run the real pipeline."
201
193
  puts " Expected behavior: headline_finder, background_brief, and"
202
194
  puts " fact_checker run in parallel; report_writer follows."
203
195
  puts
204
- puts "=" * 62
205
- puts "Example 30 complete."
206
- puts "=" * 62
196
+ hr
207
197
  exit 0
208
198
  end
209
199
 
210
- puts "── Part 3: Live LLM run (ANTHROPIC_API_KEY detected) ─────"
211
- puts
200
+ section("Part 3: Live LLM run (ANTHROPIC_API_KEY detected)")
212
201
 
213
202
  puts " Running 4-robot research pipeline on:"
214
203
  puts " \"#{topic}\""
@@ -250,6 +239,4 @@ rescue RobotLab::Error => e
250
239
  end
251
240
 
252
241
  puts
253
- puts "=" * 62
254
- puts "Example 30 complete."
255
- puts "=" * 62
242
+ hr