robot_lab-ractor 0.1.0 → 0.2.1
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/.rubocop.yml +173 -0
- data/CHANGELOG.md +15 -0
- data/Rakefile +111 -3
- 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_memory_proxy.rb +2 -4
- data/lib/robot_lab/ractor_network_scheduler.rb +55 -41
- data/lib/robot_lab/ractor_worker_pool.rb +29 -25
- metadata +9 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 25de3bcf1d2904408d989619f14f2204b556d5cc7412683793d1e108c986192f
|
|
4
|
+
data.tar.gz: 412b11c1532be8674b9ec1767471d42909dd20e98cf2ca6661f143dbeccbe68f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ce3e69d19db119d2f28e0c4b557826271f09169d8f5adaf811787f2ffba9bd637ace4aad2f5e8f525781d5c59c6e995bd851f26bac222a5e695d3ac7a6e08c1d
|
|
7
|
+
data.tar.gz: b3a6f73c02fd8e006dc71c1cea906adb1d47051c5c584d30b45ba374f9c43a036f0782a3f60a72fde5463cd78503cc7b0292a77ded5858b52d5ef9edbc693530
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
NewCops: enable
|
|
3
|
+
SuggestExtensions: false
|
|
4
|
+
TargetRubyVersion: 4.0
|
|
5
|
+
Exclude:
|
|
6
|
+
- 'examples/**/*'
|
|
7
|
+
- 'vendor/**/*'
|
|
8
|
+
- 'dead_code/**/*'
|
|
9
|
+
|
|
10
|
+
# ── Style: disabled cops ───────────────────────────────────────────────────
|
|
11
|
+
Style/StringLiterals:
|
|
12
|
+
Enabled: false
|
|
13
|
+
|
|
14
|
+
Style/StringLiteralsInInterpolation:
|
|
15
|
+
Enabled: false
|
|
16
|
+
|
|
17
|
+
Style/Documentation:
|
|
18
|
+
Enabled: false
|
|
19
|
+
|
|
20
|
+
# Ruby 4.0 freezes string literals by default
|
|
21
|
+
Style/FrozenStringLiteralComment:
|
|
22
|
+
Enabled: false
|
|
23
|
+
|
|
24
|
+
Style/IfUnlessModifier:
|
|
25
|
+
Enabled: false
|
|
26
|
+
|
|
27
|
+
Style/RescueModifier:
|
|
28
|
+
Enabled: false
|
|
29
|
+
|
|
30
|
+
Style/TrivialAccessors:
|
|
31
|
+
Enabled: false
|
|
32
|
+
|
|
33
|
+
Style/MultilineTernaryOperator:
|
|
34
|
+
Enabled: false
|
|
35
|
+
|
|
36
|
+
Style/SafeNavigation:
|
|
37
|
+
Enabled: false
|
|
38
|
+
|
|
39
|
+
Style/EmptyClassDefinition:
|
|
40
|
+
Enabled: false
|
|
41
|
+
|
|
42
|
+
Style/ClassAndModuleChildren:
|
|
43
|
+
Enabled: false
|
|
44
|
+
|
|
45
|
+
Style/RescueStandardError:
|
|
46
|
+
Enabled: false
|
|
47
|
+
|
|
48
|
+
Style/OneClassPerFile:
|
|
49
|
+
Enabled: false
|
|
50
|
+
|
|
51
|
+
# Both % and format/sprintf are acceptable
|
|
52
|
+
Style/FormatString:
|
|
53
|
+
Enabled: false
|
|
54
|
+
|
|
55
|
+
# String concatenation and interpolation are both acceptable
|
|
56
|
+
Style/StringConcatenation:
|
|
57
|
+
Enabled: false
|
|
58
|
+
|
|
59
|
+
# ── Layout ─────────────────────────────────────────────────────────────────
|
|
60
|
+
Layout/LineLength:
|
|
61
|
+
Max: 140
|
|
62
|
+
|
|
63
|
+
Layout/ExtraSpacing:
|
|
64
|
+
Enabled: false
|
|
65
|
+
|
|
66
|
+
Layout/HashAlignment:
|
|
67
|
+
Enabled: false
|
|
68
|
+
|
|
69
|
+
Layout/FirstHashElementIndentation:
|
|
70
|
+
Enabled: false
|
|
71
|
+
|
|
72
|
+
Layout/EmptyLineAfterGuardClause:
|
|
73
|
+
Enabled: false
|
|
74
|
+
|
|
75
|
+
# ── Naming ─────────────────────────────────────────────────────────────────
|
|
76
|
+
# Single-char params (c, e, n) are acceptable throughout
|
|
77
|
+
Naming/MethodParameterName:
|
|
78
|
+
Enabled: false
|
|
79
|
+
|
|
80
|
+
Naming/VariableNumber:
|
|
81
|
+
Exclude:
|
|
82
|
+
- 'test/**/*'
|
|
83
|
+
|
|
84
|
+
Naming/RescuedExceptionsVariableName:
|
|
85
|
+
Enabled: false
|
|
86
|
+
|
|
87
|
+
# set_results and similar explicit setters are clear and conventional
|
|
88
|
+
Naming/AccessorMethodName:
|
|
89
|
+
Enabled: false
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# has_tool_calls? and similar are clear and conventional
|
|
93
|
+
Naming/PredicatePrefix:
|
|
94
|
+
Enabled: false
|
|
95
|
+
|
|
96
|
+
# Test helper methods don't need to follow predicate naming rules
|
|
97
|
+
Naming/PredicateMethod:
|
|
98
|
+
Exclude:
|
|
99
|
+
- 'test/**/*'
|
|
100
|
+
|
|
101
|
+
# ── Lint: relax noisy cops on intentional patterns ─────────────────────────
|
|
102
|
+
# Library and framework methods commonly accept args for API/documentation purposes
|
|
103
|
+
Lint/UnusedMethodArgument:
|
|
104
|
+
Enabled: false
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
Lint/EmptyBlock:
|
|
108
|
+
Exclude:
|
|
109
|
+
- 'test/**/*'
|
|
110
|
+
|
|
111
|
+
Lint/ConstantDefinitionInBlock:
|
|
112
|
+
Exclude:
|
|
113
|
+
- 'Rakefile'
|
|
114
|
+
- 'test/**/*'
|
|
115
|
+
|
|
116
|
+
# ── Gemspec ────────────────────────────────────────────────────────────────
|
|
117
|
+
Gemspec/DevelopmentDependencies:
|
|
118
|
+
EnforcedStyle: Gemfile
|
|
119
|
+
|
|
120
|
+
Gemspec/RequiredRubyVersion:
|
|
121
|
+
Enabled: false
|
|
122
|
+
|
|
123
|
+
Gemspec/OrderedDependencies:
|
|
124
|
+
Enabled: false
|
|
125
|
+
|
|
126
|
+
# ── Metrics ────────────────────────────────────────────────────────────────
|
|
127
|
+
# Framework-level code (routers, parsers, orchestrators) is inherently complex.
|
|
128
|
+
# Flog is the primary complexity gate — these RuboCop thresholds catch only
|
|
129
|
+
# egregious outliers without false-positiving every dispatch method.
|
|
130
|
+
|
|
131
|
+
Metrics/MethodLength:
|
|
132
|
+
Max: 35
|
|
133
|
+
CountAsOne:
|
|
134
|
+
- heredoc
|
|
135
|
+
- array
|
|
136
|
+
- hash
|
|
137
|
+
Exclude:
|
|
138
|
+
- 'test/**/*'
|
|
139
|
+
|
|
140
|
+
Metrics/AbcSize:
|
|
141
|
+
Max: 40
|
|
142
|
+
Exclude:
|
|
143
|
+
- 'test/**/*'
|
|
144
|
+
|
|
145
|
+
Metrics/ClassLength:
|
|
146
|
+
Max: 600
|
|
147
|
+
Exclude:
|
|
148
|
+
- 'test/**/*'
|
|
149
|
+
|
|
150
|
+
Metrics/ModuleLength:
|
|
151
|
+
Max: 200
|
|
152
|
+
Exclude:
|
|
153
|
+
- 'test/**/*'
|
|
154
|
+
|
|
155
|
+
Metrics/CyclomaticComplexity:
|
|
156
|
+
Max: 20
|
|
157
|
+
Exclude:
|
|
158
|
+
- 'test/**/*'
|
|
159
|
+
|
|
160
|
+
Metrics/PerceivedComplexity:
|
|
161
|
+
Max: 20
|
|
162
|
+
Exclude:
|
|
163
|
+
- 'test/**/*'
|
|
164
|
+
|
|
165
|
+
# Long method signatures with keyword args are a Ruby framework idiom
|
|
166
|
+
Metrics/ParameterLists:
|
|
167
|
+
Enabled: false
|
|
168
|
+
|
|
169
|
+
Metrics/BlockLength:
|
|
170
|
+
Exclude:
|
|
171
|
+
- 'Rakefile'
|
|
172
|
+
- '*.gemspec'
|
|
173
|
+
- 'test/**/*'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.1] - 2026-05-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `RactorWorkerPool` — shared pool of Ractor workers; tools marked `ractor_safe true` are automatically routed through it instead of running inline
|
|
7
|
+
- `RactorNetworkScheduler` — DAG-aware parallel execution of robot networks using Ractors; activated via `parallel_mode: :ractor` on a network
|
|
8
|
+
- `RactorBoundary` — `freeze_deep` utility for making values Ractor-shareable; raises `RactorBoundaryError` for values that cannot cross Ractor boundaries
|
|
9
|
+
- `RactorMemoryProxy` — thread-safe proxy exposing `RobotLab::Memory` to Ractor workers via `Ractor::Wrapper`
|
|
10
|
+
- `RactorJob` — Ractor-shareable frozen job value object used internally by the pool and scheduler
|
|
11
|
+
- `ractor_safe` class-level DSL for `RubyLLM::Tool` / `RobotLab::Tool` subclasses; inherited by subclasses
|
|
12
|
+
- `RobotLab.ractor_pool` / `.shutdown_ractor_pool` — process-level pool lifecycle methods added to the `RobotLab` module
|
|
13
|
+
- Ractor Parallelism guide (`docs/guides/ractor-parallelism.md`) covering both CPU-bound tools and parallel network pipelines
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Version synchronized with robot_lab core 0.2.1
|
|
17
|
+
|
|
3
18
|
## [0.1.0] - 2026-05-07
|
|
4
19
|
|
|
5
20
|
- Initial release
|
data/Rakefile
CHANGED
|
@@ -1,8 +1,116 @@
|
|
|
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 style with RuboCop'
|
|
28
|
+
task :rubocop do
|
|
29
|
+
sh 'bundle exec rubocop'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
desc 'Auto-correct RuboCop offenses'
|
|
33
|
+
task :rubocop_fix do
|
|
34
|
+
sh 'bundle exec rubocop -a'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
desc 'Check code complexity with Flog (warn >=20, fail >=50)'
|
|
38
|
+
task :flog_check do
|
|
39
|
+
require 'flog'
|
|
40
|
+
|
|
41
|
+
method_warn = 20.0
|
|
42
|
+
method_fail = 50.0
|
|
43
|
+
|
|
44
|
+
flogger = Flog.new(all: true)
|
|
45
|
+
flogger.flog(*Dir.glob('lib/**/*.rb'))
|
|
46
|
+
|
|
47
|
+
warnings = []
|
|
48
|
+
failures = []
|
|
49
|
+
|
|
50
|
+
flogger.each_by_score do |method, score|
|
|
51
|
+
next if method.end_with?('#none')
|
|
52
|
+
|
|
53
|
+
if score > method_fail
|
|
54
|
+
failures << "#{format('%.1f', score)}: #{method}"
|
|
55
|
+
elsif score > method_warn
|
|
56
|
+
warnings << "#{format('%.1f', score)}: #{method}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
unless warnings.empty?
|
|
61
|
+
puts "\nFlog warnings (#{method_warn}–#{method_fail}) — target for future refactoring:"
|
|
62
|
+
warnings.each { |v| puts " #{v}" }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if failures.empty?
|
|
66
|
+
puts "\nFlog: no methods exceed the failure threshold (>=#{method_fail})"
|
|
67
|
+
else
|
|
68
|
+
puts "\nFlog failures (>=#{method_fail}) — must be refactored:"
|
|
69
|
+
failures.each { |v| puts " #{v}" }
|
|
70
|
+
abort "\nFlog quality gate failed: #{failures.size} method(s) exceed #{method_fail}"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
desc 'Run all quality checks: tests (with coverage), RuboCop, and Flog'
|
|
75
|
+
task :quality do
|
|
76
|
+
results = {}
|
|
77
|
+
|
|
78
|
+
puts "\n#{'=' * 60}"
|
|
79
|
+
puts 'Quality Gate: Tests + Coverage'
|
|
80
|
+
puts '=' * 60
|
|
81
|
+
results[:tests] = system('bundle exec rake test') ? :pass : :fail
|
|
82
|
+
|
|
83
|
+
puts "\n#{'=' * 60}"
|
|
84
|
+
puts 'Quality Gate: RuboCop'
|
|
85
|
+
puts '=' * 60
|
|
86
|
+
results[:rubocop] = system('bundle exec rubocop') ? :pass : :fail
|
|
87
|
+
|
|
88
|
+
puts "\n#{'=' * 60}"
|
|
89
|
+
puts 'Quality Gate: Flog Complexity'
|
|
90
|
+
puts '=' * 60
|
|
91
|
+
results[:flog] = system('bundle exec rake flog_check') ? :pass : :fail
|
|
92
|
+
|
|
93
|
+
puts "\n#{'=' * 60}"
|
|
94
|
+
puts 'Quality Summary'
|
|
95
|
+
puts '=' * 60
|
|
96
|
+
results.each do |gate, status|
|
|
97
|
+
icon = status == :pass ? 'PASS' : 'FAIL'
|
|
98
|
+
puts " [#{icon}] #{gate}"
|
|
99
|
+
end
|
|
100
|
+
puts '=' * 60
|
|
101
|
+
|
|
102
|
+
abort "\nQuality gate failed" if results.values.any?(:fail)
|
|
103
|
+
puts "\nAll quality gates passed."
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
namespace :docs do
|
|
107
|
+
desc 'Build MkDocs documentation'
|
|
108
|
+
task :build do
|
|
109
|
+
sh 'mkdocs build'
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
desc 'Serve MkDocs documentation locally on http://localhost:8000'
|
|
113
|
+
task :serve do
|
|
114
|
+
sh 'mkdocs serve'
|
|
115
|
+
end
|
|
116
|
+
end
|
data/lib/robot_lab/ractor.rb
CHANGED
|
@@ -5,16 +5,16 @@
|
|
|
5
5
|
# code inside the RobotLab namespace, breaking Ractor.new / Ractor.make_shareable
|
|
6
6
|
# calls in RactorWorkerPool, RactorNetworkScheduler, etc.
|
|
7
7
|
|
|
8
|
-
require
|
|
9
|
-
require
|
|
10
|
-
require
|
|
8
|
+
require 'etc'
|
|
9
|
+
require 'ractor_queue'
|
|
10
|
+
require 'ractor/wrapper'
|
|
11
11
|
|
|
12
|
-
require_relative
|
|
13
|
-
require_relative
|
|
14
|
-
require_relative
|
|
15
|
-
require_relative
|
|
16
|
-
require_relative
|
|
17
|
-
require_relative
|
|
12
|
+
require_relative 'ractor/version'
|
|
13
|
+
require_relative 'ractor_job'
|
|
14
|
+
require_relative 'ractor_boundary'
|
|
15
|
+
require_relative 'ractor_worker_pool'
|
|
16
|
+
require_relative 'ractor_memory_proxy'
|
|
17
|
+
require_relative 'ractor_network_scheduler'
|
|
18
18
|
|
|
19
19
|
# Extend the RobotLab module with Ractor pool lifecycle methods.
|
|
20
20
|
# Once robot_lab removes its own copies and adds robot_lab-ractor as a
|
|
@@ -23,8 +23,11 @@ module RobotLab
|
|
|
23
23
|
class << self
|
|
24
24
|
def ractor_pool
|
|
25
25
|
@ractor_pool ||= begin
|
|
26
|
-
size = respond_to?(:config) && config.respond_to?(:ractor_pool_size)
|
|
27
|
-
|
|
26
|
+
size = if respond_to?(:config) && config.respond_to?(:ractor_pool_size)
|
|
27
|
+
config.ractor_pool_size || :auto
|
|
28
|
+
else
|
|
29
|
+
:auto
|
|
30
|
+
end
|
|
28
31
|
RactorWorkerPool.new(size: size)
|
|
29
32
|
end
|
|
30
33
|
end
|
|
@@ -35,3 +38,7 @@ module RobotLab
|
|
|
35
38
|
end
|
|
36
39
|
end
|
|
37
40
|
end
|
|
41
|
+
|
|
42
|
+
if defined?(RobotLab) && RobotLab.respond_to?(:register_extension)
|
|
43
|
+
RobotLab.register_extension(:ractor, :ractor_extension_loaded)
|
|
44
|
+
end
|
|
@@ -31,9 +31,7 @@ module RobotLab
|
|
|
31
31
|
|
|
32
32
|
# Returns the Ractor-shareable stub for use inside Ractors.
|
|
33
33
|
# @return [Ractor::Wrapper stub]
|
|
34
|
-
|
|
35
|
-
@stub
|
|
36
|
-
end
|
|
34
|
+
attr_reader :stub
|
|
37
35
|
|
|
38
36
|
# Read a value from the proxied Memory.
|
|
39
37
|
# @param key [Symbol]
|
|
@@ -61,7 +59,7 @@ module RobotLab
|
|
|
61
59
|
def shutdown
|
|
62
60
|
@wrapper.async_stop
|
|
63
61
|
@wrapper.join
|
|
64
|
-
rescue => e
|
|
62
|
+
rescue StandardError => e
|
|
65
63
|
warn "RactorMemoryProxy shutdown error: #{e.message}"
|
|
66
64
|
end
|
|
67
65
|
end
|
|
@@ -50,21 +50,10 @@ module RobotLab
|
|
|
50
50
|
remaining = specs_with_deps.dup
|
|
51
51
|
|
|
52
52
|
until remaining.empty?
|
|
53
|
-
ready, remaining = remaining
|
|
54
|
-
|
|
55
|
-
deps == :none || deps == :optional ||
|
|
56
|
-
Array(deps).all? { |d| completed.key?(d) }
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
raise RobotLab::Error, "Circular dependency or unresolvable deps in RactorNetworkScheduler" if ready.empty?
|
|
53
|
+
ready, remaining = partition_ready(remaining, completed)
|
|
54
|
+
raise RobotLab::Error, 'Circular dependency or unresolvable deps in RactorNetworkScheduler' if ready.empty?
|
|
60
55
|
|
|
61
|
-
|
|
62
|
-
spec = entry[:spec]
|
|
63
|
-
msg = completed.values.last || message
|
|
64
|
-
Thread.new { [spec.name, execute_spec(spec, msg)] }.tap { |t| t.report_on_exception = false }
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
threads.each do |t|
|
|
56
|
+
dispatch_ready(ready, completed, message).each do |t|
|
|
68
57
|
name, result = t.value
|
|
69
58
|
completed[name] = result
|
|
70
59
|
end
|
|
@@ -79,7 +68,38 @@ module RobotLab
|
|
|
79
68
|
|
|
80
69
|
@closed = true
|
|
81
70
|
@size.times { @work_q.push(nil) }
|
|
82
|
-
@workers.each
|
|
71
|
+
@workers.each do |w|
|
|
72
|
+
w.join
|
|
73
|
+
rescue StandardError
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Called inside Ractor worker blocks — must be a class method.
|
|
79
|
+
def self.process_job(job)
|
|
80
|
+
spec = job.payload[:spec]
|
|
81
|
+
message = job.payload[:message]
|
|
82
|
+
job.reply_queue.push(build_and_run_robot(spec, message))
|
|
83
|
+
rescue StandardError => e
|
|
84
|
+
job.reply_queue.push(wrap_error(e))
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.build_and_run_robot(spec, message)
|
|
88
|
+
config = spec.config_hash.empty? ? nil : RobotLab::RunConfig.new(**spec.config_hash.transform_keys(&:to_sym))
|
|
89
|
+
robot = RobotLab::Robot.new(
|
|
90
|
+
name: spec.name,
|
|
91
|
+
template: spec.template&.to_sym,
|
|
92
|
+
system_prompt: spec.system_prompt,
|
|
93
|
+
config: config
|
|
94
|
+
)
|
|
95
|
+
robot.run(message).last_text_content.to_s.freeze
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def self.wrap_error(err)
|
|
99
|
+
RobotLab::RactorJobError.new(
|
|
100
|
+
message: err.message.freeze,
|
|
101
|
+
backtrace: (err.backtrace || []).map(&:freeze).freeze
|
|
102
|
+
)
|
|
83
103
|
end
|
|
84
104
|
|
|
85
105
|
private
|
|
@@ -90,18 +110,16 @@ module RobotLab
|
|
|
90
110
|
reply_q = RactorQueue.new(capacity: 1)
|
|
91
111
|
|
|
92
112
|
job = RactorJob.new(
|
|
93
|
-
id:
|
|
94
|
-
type:
|
|
95
|
-
payload:
|
|
113
|
+
id: SecureRandom.uuid.freeze,
|
|
114
|
+
type: :robot,
|
|
115
|
+
payload: RactorBoundary.freeze_deep({ spec: frozen_spec, message: frozen_message }),
|
|
96
116
|
reply_queue: reply_q
|
|
97
117
|
)
|
|
98
118
|
|
|
99
119
|
@work_q.push(job)
|
|
100
120
|
result = reply_q.pop
|
|
101
121
|
|
|
102
|
-
if result.is_a?(RactorJobError)
|
|
103
|
-
raise RobotLab::Error, "Robot '#{spec.name}' failed in Ractor: #{result.message}"
|
|
104
|
-
end
|
|
122
|
+
raise RobotLab::Error, "Robot '#{spec.name}' failed in Ractor: #{result.message}" if result.is_a?(RactorJobError)
|
|
105
123
|
|
|
106
124
|
result
|
|
107
125
|
end
|
|
@@ -112,28 +130,24 @@ module RobotLab
|
|
|
112
130
|
job = q.pop
|
|
113
131
|
break if job.nil?
|
|
114
132
|
|
|
115
|
-
|
|
116
|
-
spec = job.payload[:spec]
|
|
117
|
-
message = job.payload[:message]
|
|
118
|
-
|
|
119
|
-
robot = RobotLab::Robot.new(
|
|
120
|
-
name: spec.name,
|
|
121
|
-
template: spec.template ? spec.template.to_sym : nil,
|
|
122
|
-
system_prompt: spec.system_prompt,
|
|
123
|
-
config: spec.config_hash.empty? ? nil : RobotLab::RunConfig.new(**spec.config_hash.transform_keys(&:to_sym))
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
robot_result = robot.run(message)
|
|
127
|
-
job.reply_queue.push(robot_result.last_text_content.to_s.freeze)
|
|
128
|
-
rescue => e
|
|
129
|
-
err = RobotLab::RactorJobError.new(
|
|
130
|
-
message: e.message.freeze,
|
|
131
|
-
backtrace: (e.backtrace || []).map(&:freeze).freeze
|
|
132
|
-
)
|
|
133
|
-
job.reply_queue.push(err)
|
|
134
|
-
end
|
|
133
|
+
RobotLab::RactorNetworkScheduler.process_job(job)
|
|
135
134
|
end
|
|
136
135
|
end
|
|
137
136
|
end
|
|
137
|
+
|
|
138
|
+
def partition_ready(remaining, completed)
|
|
139
|
+
remaining.partition do |entry|
|
|
140
|
+
deps = entry[:depends_on]
|
|
141
|
+
deps == :none || deps == :optional || Array(deps).all? { |d| completed.key?(d) }
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def dispatch_ready(ready, completed, message)
|
|
146
|
+
ready.map do |entry|
|
|
147
|
+
spec = entry[:spec]
|
|
148
|
+
msg = completed.values.last || message
|
|
149
|
+
Thread.new { [spec.name, execute_spec(spec, msg)] }.tap { |t| t.report_on_exception = false }
|
|
150
|
+
end
|
|
151
|
+
end
|
|
138
152
|
end
|
|
139
153
|
end
|
|
@@ -39,27 +39,22 @@ module RobotLab
|
|
|
39
39
|
# @return [Object] the tool's return value
|
|
40
40
|
# @raise [ToolError] if the tool raises inside the Ractor
|
|
41
41
|
def submit(tool_class_name, args)
|
|
42
|
-
raise ToolError,
|
|
42
|
+
raise ToolError, 'Pool is shut down' if @closed
|
|
43
43
|
|
|
44
44
|
reply_q = RactorQueue.new(capacity: 1)
|
|
45
|
-
payload = RactorBoundary.freeze_deep({
|
|
46
|
-
tool_class: tool_class_name.to_s,
|
|
47
|
-
args: args
|
|
48
|
-
})
|
|
45
|
+
payload = RactorBoundary.freeze_deep({ tool_class: tool_class_name.to_s, args: args })
|
|
49
46
|
|
|
50
47
|
job = RactorJob.new(
|
|
51
|
-
id:
|
|
52
|
-
type:
|
|
53
|
-
payload:
|
|
48
|
+
id: SecureRandom.uuid.freeze,
|
|
49
|
+
type: :tool,
|
|
50
|
+
payload: payload,
|
|
54
51
|
reply_queue: reply_q
|
|
55
52
|
)
|
|
56
53
|
|
|
57
54
|
@work_q.push(job)
|
|
58
55
|
result = reply_q.pop
|
|
59
56
|
|
|
60
|
-
if result.is_a?(RactorJobError)
|
|
61
|
-
raise ToolError, "Tool '#{tool_class_name}' failed in Ractor: #{result.message}"
|
|
62
|
-
end
|
|
57
|
+
raise ToolError, "Tool '#{tool_class_name}' failed in Ractor: #{result.message}" if result.is_a?(RactorJobError)
|
|
63
58
|
|
|
64
59
|
result
|
|
65
60
|
end
|
|
@@ -71,7 +66,28 @@ module RobotLab
|
|
|
71
66
|
|
|
72
67
|
@closed = true
|
|
73
68
|
@size.times { @work_q.push(nil) }
|
|
74
|
-
@workers.each
|
|
69
|
+
@workers.each do |w|
|
|
70
|
+
w.join
|
|
71
|
+
rescue StandardError
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Called inside Ractor worker blocks — must be a class method.
|
|
77
|
+
def self.process_job(job)
|
|
78
|
+
tool_class = Object.const_get(job.payload[:tool_class])
|
|
79
|
+
result = tool_class.new.execute(**job.payload[:args].transform_keys(&:to_sym))
|
|
80
|
+
frozen_result = ::Ractor.make_shareable(result.frozen? ? result : result.dup.freeze)
|
|
81
|
+
job.reply_queue.push(frozen_result)
|
|
82
|
+
rescue StandardError => e
|
|
83
|
+
job.reply_queue.push(wrap_error(e))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def self.wrap_error(err)
|
|
87
|
+
RobotLab::RactorJobError.new(
|
|
88
|
+
message: err.message.freeze,
|
|
89
|
+
backtrace: (err.backtrace || []).map(&:freeze).freeze
|
|
90
|
+
)
|
|
75
91
|
end
|
|
76
92
|
|
|
77
93
|
private
|
|
@@ -82,19 +98,7 @@ module RobotLab
|
|
|
82
98
|
job = q.pop
|
|
83
99
|
break if job.nil?
|
|
84
100
|
|
|
85
|
-
|
|
86
|
-
tool_class = Object.const_get(job.payload[:tool_class])
|
|
87
|
-
tool = tool_class.new
|
|
88
|
-
result = tool.execute(**job.payload[:args].transform_keys(&:to_sym))
|
|
89
|
-
frozen_result = ::Ractor.make_shareable(result.frozen? ? result : result.dup.freeze)
|
|
90
|
-
job.reply_queue.push(frozen_result)
|
|
91
|
-
rescue => e
|
|
92
|
-
err = RobotLab::RactorJobError.new(
|
|
93
|
-
message: e.message.freeze,
|
|
94
|
-
backtrace: (e.backtrace || []).map(&:freeze).freeze
|
|
95
|
-
)
|
|
96
|
-
job.reply_queue.push(err)
|
|
97
|
-
end
|
|
101
|
+
RobotLab::RactorWorkerPool.process_job(job)
|
|
98
102
|
end
|
|
99
103
|
end
|
|
100
104
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: robot_lab-ractor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dewayne VanHoozer
|
|
@@ -10,7 +10,7 @@ cert_chain: []
|
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
|
-
name:
|
|
13
|
+
name: ractor_queue
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
@@ -24,7 +24,7 @@ dependencies:
|
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '0'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
|
-
name:
|
|
27
|
+
name: ractor-wrapper
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
30
|
- - ">="
|
|
@@ -38,19 +38,19 @@ dependencies:
|
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '0'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
|
-
name:
|
|
41
|
+
name: robot_lab
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
|
-
- - "
|
|
44
|
+
- - "~>"
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version:
|
|
46
|
+
version: 0.2.0
|
|
47
47
|
type: :runtime
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
|
-
- - "
|
|
51
|
+
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version:
|
|
53
|
+
version: 0.2.0
|
|
54
54
|
description: Provides RobotLab::RactorWorkerPool — a pool of Ruby Ractor workers for
|
|
55
55
|
executing CPU-bound, Ractor-safe tools in parallel. Includes RactorBoundary (deep-freeze
|
|
56
56
|
utility for crossing Ractor boundaries), RactorJob/RactorJobError/RobotSpec (frozen
|
|
@@ -64,6 +64,7 @@ extra_rdoc_files: []
|
|
|
64
64
|
files:
|
|
65
65
|
- ".envrc"
|
|
66
66
|
- ".github/workflows/deploy-github-pages.yml"
|
|
67
|
+
- ".rubocop.yml"
|
|
67
68
|
- CHANGELOG.md
|
|
68
69
|
- LICENSE.txt
|
|
69
70
|
- README.md
|