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 +4 -4
- data/.envrc +2 -0
- data/.loki +10 -0
- data/.quality/reek_baseline.txt +3 -0
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +25 -0
- data/CLAUDE.md +71 -0
- data/Rakefile +127 -3
- data/docs/guides/ractor-parallelism.md +75 -0
- data/docs/index.md +6 -0
- data/examples/{29_ractor_tools.rb → 01_ractor_tools.rb} +9 -35
- data/examples/{30_ractor_network.rb → 02_ractor_network.rb} +8 -21
- data/examples/03_ractor_hooks.rb +399 -0
- data/examples/common.rb +79 -0
- data/examples/xyzzy.rb +88 -0
- data/lib/robot_lab/ractor/version.rb +1 -1
- data/lib/robot_lab/ractor.rb +18 -11
- data/lib/robot_lab/ractor_boundary.rb +1 -1
- data/lib/robot_lab/ractor_job.rb +5 -1
- data/lib/robot_lab/ractor_memory_proxy.rb +2 -4
- data/lib/robot_lab/ractor_network_scheduler.rb +56 -41
- data/lib/robot_lab/ractor_worker_pool.rb +29 -25
- metadata +18 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7df9c3ab4660c44f064946a5a90f071df1ad03baa1b4cac8c3259156a35be70b
|
|
4
|
+
data.tar.gz: 0fb20202b60255f45be1b423d5fa5870730cfd4fbf3dbaee1ec601eb72a87f59
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '0418c60678b46d1c7b9e3a625f8e2c3b1ebf789a2f526daea6f05d5c8a9046a11e54217bea1dd8fd15a50bae6115ea634907e002adc96d7d06f736c626f05d20'
|
|
7
|
+
data.tar.gz: c8800eee270d7987d17658a97c7dce82074ce8db09b67d7a598d92c1e29b15138d07e242f4a5de86fe0a812cadbb10c0ce02736da6d80f60cea36f24b22b4779
|
data/.envrc
CHANGED
data/.loki
ADDED
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
|
|
4
|
-
require
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
require 'rake/testtask'
|
|
5
5
|
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
require "robot_lab/ractor"
|
|
33
|
+
require_relative "common"
|
|
35
34
|
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
205
|
-
puts "Example 30 complete."
|
|
206
|
-
puts "=" * 62
|
|
196
|
+
hr
|
|
207
197
|
exit 0
|
|
208
198
|
end
|
|
209
199
|
|
|
210
|
-
|
|
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
|
-
|
|
254
|
-
puts "Example 30 complete."
|
|
255
|
-
puts "=" * 62
|
|
242
|
+
hr
|