robot_lab-ractor 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 24693d3b0371d41a878e7112c904e01bbf65d46f4cded26fe78b45856c49716c
4
+ data.tar.gz: e7fdf7b457efb4e5359f9ccf36afd50823f2fe15d11e5c56d6dd2fe93ba9f959
5
+ SHA512:
6
+ metadata.gz: 5ed65c66df3ef0d840ab8f4745cf2a3b2fe82c6216f0458480336ec3274fafde8ce8b2c610526de66f00cdf9473ab52961b6f818166b64752a911a377cb21a34
7
+ data.tar.gz: 9024af8c7f52c239b8549e04eab3c9f852622264a3f3656aac4d41cfc72ccb41707b07ec7749622aaddb4c8ff95d21af0c7e0aa106bedf25c4a58d7de15e9095
data/.envrc ADDED
@@ -0,0 +1 @@
1
+ export RR=`pwd`
@@ -0,0 +1,52 @@
1
+ name: Deploy Documentation to GitHub Pages
2
+ on:
3
+ push:
4
+ branches:
5
+ - main
6
+ - develop
7
+ paths:
8
+ - "docs/**"
9
+ - "mkdocs.yml"
10
+ - ".github/workflows/deploy-github-pages.yml"
11
+ workflow_dispatch:
12
+
13
+ permissions:
14
+ contents: write
15
+ pages: write
16
+ id-token: write
17
+
18
+ jobs:
19
+ deploy:
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - name: Checkout code
23
+ uses: actions/checkout@v4
24
+ with:
25
+ fetch-depth: 0
26
+
27
+ - name: Setup Python
28
+ uses: actions/setup-python@v5
29
+ with:
30
+ python-version: 3.x
31
+
32
+ - name: Install dependencies
33
+ run: |
34
+ pip install mkdocs
35
+ pip install mkdocs-material
36
+ pip install mkdocs-macros-plugin
37
+ pip install mike
38
+
39
+ - name: Configure Git
40
+ run: |
41
+ git config --local user.email "action@github.com"
42
+ git config --local user.name "GitHub Action"
43
+
44
+ - name: Build MkDocs site
45
+ run: mkdocs build
46
+
47
+ - name: Deploy to GitHub Pages
48
+ uses: peaceiris/actions-gh-pages@v4
49
+ with:
50
+ github_token: ${{ secrets.GITHUB_TOKEN }}
51
+ publish_dir: ./site
52
+ keep_files: true
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-05-07
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Dewayne VanHoozer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # robot_lab-ractor
2
+
3
+ Ractor-based CPU parallelism for the [RobotLab](https://github.com/MadBomber/robot_lab) LLM agent framework.
4
+
5
+ > [!CAUTION]
6
+ > This gem is under active development. APIs may change without notice.
7
+
8
+ ## What it provides
9
+
10
+ - **`RactorWorkerPool`** — shared pool of Ractor workers for CPU-bound tools; tools marked `ractor_safe true` are routed through it automatically
11
+ - **`RactorNetworkScheduler`** — DAG-aware parallel execution of robot pipelines using Ractors
12
+ - **`RactorBoundary`** — deep-freeze utilities for safely sharing objects across Ractor boundaries
13
+ - **`RactorMemoryProxy`** — thread-safe proxy exposing `RobotLab::Memory` to Ractor workers via `Ractor::Wrapper`
14
+ - **`RobotLab.ractor_pool` / `.shutdown_ractor_pool`** — process-level pool management added to the `RobotLab` module
15
+
16
+ ## Installation
17
+
18
+ Add to your Gemfile:
19
+
20
+ ```ruby
21
+ gem "robot_lab"
22
+ gem "robot_lab-ractor"
23
+ ```
24
+
25
+ ## CPU-Bound Tools
26
+
27
+ Mark a tool `ractor_safe true` and RobotLab automatically routes its calls through the global `RactorWorkerPool` instead of running inline:
28
+
29
+ ```ruby
30
+ class TranscribeAudio < RubyLLM::Tool
31
+ ractor_safe true
32
+ description "Transcribe an audio file"
33
+ param :path, type: :string, desc: "Path to audio file"
34
+
35
+ def execute(path:)
36
+ AudioTranscriber.run(path) # pure computation, no shared mutable state
37
+ end
38
+ end
39
+
40
+ robot = RobotLab.build(
41
+ name: "transcriber",
42
+ system_prompt: "You transcribe audio files.",
43
+ local_tools: [TranscribeAudio]
44
+ )
45
+
46
+ result = robot.run("Transcribe /recordings/meeting.mp3")
47
+ puts result.last_text_content
48
+ ```
49
+
50
+ ## Parallel Robot Networks
51
+
52
+ Pass `parallel_mode: :ractor` when creating a network to dispatch independent robots across hardware threads simultaneously:
53
+
54
+ ```ruby
55
+ network = RobotLab.create_network(name: "analysis", parallel_mode: :ractor) do
56
+ task :fetch, fetcher_robot, depends_on: :none
57
+ task :sentiment, sentiment_robot, depends_on: [:fetch]
58
+ task :entities, entity_robot, depends_on: [:fetch] # runs in parallel with :sentiment
59
+ task :summarize, summary_robot, depends_on: [:sentiment, :entities]
60
+ end
61
+
62
+ results = network.run(message: "Analyze customer feedback")
63
+ # => { "fetch" => "...", "sentiment" => "positive", "entities" => "...", "summarize" => "..." }
64
+ ```
65
+
66
+ The scheduler builds a DAG from the `depends_on:` declarations and fires each stage as soon as its dependencies resolve.
67
+
68
+ ## Pool Management
69
+
70
+ ```ruby
71
+ # Pool is lazily created on first use
72
+ pool = RobotLab.ractor_pool
73
+
74
+ # Drain and shut down explicitly (e.g. at process exit)
75
+ RobotLab.shutdown_ractor_pool
76
+ ```
77
+
78
+ ## Constraints
79
+
80
+ Because Ractors are isolated execution contexts that bypass Ruby's GVL, objects passed into them must be deeply frozen (no shared mutable state). The `RactorBoundary` utility handles this automatically for tool arguments. Your tool's `execute` method must not reference any unfrozen constants or class-level mutable state.
81
+
82
+ ## Links
83
+
84
+ - [Ractor Parallelism Guide](https://madbomber.github.io/robot_lab-ractor/guides/ractor-parallelism)
85
+ - [RobotLab Core](https://github.com/MadBomber/robot_lab)
86
+ - [RubyGems](https://rubygems.org/gems/robot_lab-ractor)
87
+
88
+ ## License
89
+
90
+ MIT License - Copyright (c) 2025 Dewayne VanHoozer
91
+
92
+ ## Contributing
93
+
94
+ Bug reports and pull requests are welcome on GitHub at https://github.com/MadBomber/robot_lab-ractor.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ task default: :test
@@ -0,0 +1,364 @@
1
+ # Ractor Parallelism
2
+
3
+ RobotLab supports true CPU parallelism via Ruby Ractors — isolated execution contexts that bypass the Global VM Lock (GVL). This guide explains how to put both CPU-bound tools and multi-robot pipelines on parallel hardware threads.
4
+
5
+ ## Why Ractors?
6
+
7
+ Ruby's standard thread model is I/O-concurrent but CPU-serialized: the GVL means only one thread runs Ruby code at a time. For LLM workflows this is usually fine — robots spend most of their time waiting on the network. But some workloads benefit from real parallel execution:
8
+
9
+ - **CPU-intensive tools** — text processing, image analysis, embeddings, cryptography
10
+ - **Independent robot pipelines** — multiple robots working on unrelated subtasks simultaneously
11
+
12
+ Ractors bypass the GVL entirely. Each Ractor runs on its own OS thread with no shared mutable state, so multiple Ractors genuinely execute in parallel on multi-core hardware.
13
+
14
+ ## Architecture Overview
15
+
16
+ RobotLab provides two parallel tracks:
17
+
18
+ ```
19
+ ┌─────────────────────────────────────────────────────┐
20
+ │ Your Application │
21
+ ├─────────────────────────────┬───────────────────────┤
22
+ │ Track 1: CPU-bound Tools │ Track 2: Robots │
23
+ │ │ │
24
+ │ Tool#ractor_safe │ Network │
25
+ │ ↓ │ parallel_mode: :ractor│
26
+ │ RactorWorkerPool │ ↓ │
27
+ │ (N Ractor workers) │ RactorNetworkScheduler│
28
+ │ │ (N Ractor workers) │
29
+ ├─────────────────────────────┴───────────────────────┤
30
+ │ Shared Infrastructure │
31
+ │ RactorBoundary · RactorJob · RactorMemoryProxy │
32
+ └─────────────────────────────────────────────────────┘
33
+ ```
34
+
35
+ **Track 1** routes Ractor-safe tools through a global worker pool instead of calling them inline. The robot never notices — it still gets back a result string.
36
+
37
+ **Track 2** replaces the `SimpleFlow::Pipeline` executor for a network with a `RactorNetworkScheduler` that dispatches frozen robot specs to Ractor workers, respecting `depends_on` ordering.
38
+
39
+ Both tracks share the same frozen-data convention: all values crossing a Ractor boundary must be Ractor-shareable.
40
+
41
+ ---
42
+
43
+ ## Track 1: CPU-Bound Tools
44
+
45
+ ### Declaring a Tool as Ractor-Safe
46
+
47
+ Add `ractor_safe true` to any `RubyLLM::Tool` or `RobotLab::Tool` subclass:
48
+
49
+ ```ruby
50
+ class TranscribeAudio < RubyLLM::Tool
51
+ ractor_safe true
52
+
53
+ description "Transcribe an audio file to text"
54
+
55
+ param :path, type: :string, desc: "Absolute path to the audio file"
56
+ param :format, type: :string, desc: "Audio format (wav, mp3, ogg)", required: false
57
+
58
+ def execute(path:, format: "wav")
59
+ # Pure computation — no shared mutable state, no IO closures
60
+ AudioTranscriber.run(path, format: format)
61
+ end
62
+ end
63
+ ```
64
+
65
+ When a robot calls this tool, RobotLab automatically routes the call through the global `RactorWorkerPool` rather than executing it inline. The robot is unaffected — it receives the result string as normal.
66
+
67
+ `ractor_safe` is inherited. If you declare it on a base class, all subclasses are also treated as Ractor-safe:
68
+
69
+ ```ruby
70
+ class BaseAudioTool < RubyLLM::Tool
71
+ ractor_safe true
72
+ end
73
+
74
+ class TranscribeAudio < BaseAudioTool # also ractor_safe
75
+ # ...
76
+ end
77
+
78
+ class DetectLanguage < BaseAudioTool # also ractor_safe
79
+ # ...
80
+ end
81
+ ```
82
+
83
+ ### What Makes a Tool Ractor-Safe?
84
+
85
+ A tool is safe to run inside a Ractor when its `execute` method:
86
+
87
+ - Uses only **frozen or locally-created** objects
88
+ - Does **not** read or write class-level mutable state (class variables, module-level globals)
89
+ - Does **not** hold references to closures, Procs, or lambdas defined outside the Ractor
90
+ - Does **not** use non-Ractor-safe C extensions (most pure-Ruby code is fine)
91
+
92
+ ```ruby
93
+ # Safe: all inputs arrive as frozen args; result is fresh
94
+ class HashContent < RubyLLM::Tool
95
+ ractor_safe true
96
+ description "SHA-256 hash of a string"
97
+ param :text, type: :string, desc: "Text to hash"
98
+
99
+ def execute(text:)
100
+ require "digest"
101
+ Digest::SHA256.hexdigest(text)
102
+ end
103
+ end
104
+
105
+ # Not safe: reads and writes @@cache (shared mutable state)
106
+ class CachedLookup < RubyLLM::Tool
107
+ @@cache = {} # mutable class variable — NOT Ractor-safe
108
+
109
+ def execute(key:)
110
+ @@cache[key] ||= expensive_lookup(key)
111
+ end
112
+ end
113
+ ```
114
+
115
+ ### Configuring the Worker Pool
116
+
117
+ The global pool is created lazily on first use. You can control its size through `RunConfig`:
118
+
119
+ ```ruby
120
+ RobotLab.configure do |config|
121
+ config.ractor_pool_size = 8 # default: Etc.nprocessors
122
+ end
123
+ ```
124
+
125
+ Or per-robot / per-network via `RunConfig`:
126
+
127
+ ```ruby
128
+ config = RobotLab::RunConfig.new(ractor_pool_size: 4)
129
+ robot = RobotLab.build(name: "cruncher", config: config, ...)
130
+ ```
131
+
132
+ Access the shared pool directly:
133
+
134
+ ```ruby
135
+ pool = RobotLab.ractor_pool # RactorWorkerPool instance
136
+ RobotLab.shutdown_ractor_pool # graceful shutdown (poison-pill pattern)
137
+ ```
138
+
139
+ ---
140
+
141
+ ## Track 2: Parallel Robot Networks
142
+
143
+ ### Enabling Ractor Mode
144
+
145
+ Pass `parallel_mode: :ractor` when creating a network:
146
+
147
+ ```ruby
148
+ network = RobotLab.create_network(name: "analysis", parallel_mode: :ractor) do
149
+ task :fetch, fetcher_robot, depends_on: :none
150
+ task :sentiment, sentiment_robot, depends_on: [:fetch]
151
+ task :entities, entity_robot, depends_on: [:fetch]
152
+ task :summarize, summary_robot, depends_on: [:sentiment, :entities]
153
+ end
154
+
155
+ result = network.run(message: "Analyze customer feedback")
156
+ ```
157
+
158
+ When `parallel_mode: :ractor` is set, `Network#run` delegates to `RactorNetworkScheduler` instead of the default `SimpleFlow::Pipeline` executor. The default is `:async` (unchanged behavior).
159
+
160
+ ### How It Works
161
+
162
+ The scheduler builds a `RobotSpec` — a frozen, Ractor-shareable description — for each robot in the network, then dispatches them in dependency order:
163
+
164
+ 1. **Partition** tasks into waves: tasks whose dependencies are all resolved are dispatched together.
165
+ 2. **Each wave** spawns one thread per task; each thread submits a `RactorJob` to the shared work queue and blocks on the per-job reply queue.
166
+ 3. **Worker Ractors** pop jobs, construct a fresh `Robot` from the spec, call `robot.run(message)`, and push the frozen result string back.
167
+ 4. **LLM calls** (ruby_llm) always happen in threads — Ractors hand off network I/O naturally since the thread is doing the blocking.
168
+
169
+ ```
170
+ Wave 1: [ fetch ]
171
+ ↓ result passed to next wave
172
+ Wave 2: [ sentiment | entities ] ← run in parallel
173
+ ↓ both results available
174
+ Wave 3: [ summarize ]
175
+ ```
176
+
177
+ The return value of `run` is a `Hash` mapping robot name strings to their result strings:
178
+
179
+ ```ruby
180
+ results = network.run(message: "Analyze this")
181
+ # => { "fetch" => "...", "sentiment" => "positive", "entities" => "...", "summarize" => "..." }
182
+ ```
183
+
184
+ ### Dependency Ordering
185
+
186
+ Dependency semantics mirror those of `SimpleFlow::Pipeline`:
187
+
188
+ | `depends_on` value | Meaning |
189
+ |---|---|
190
+ | `:none` | Entry-point task; dispatched in the first wave |
191
+ | `:optional` | Runs in the first wave (not blocked by anything) |
192
+ | `["task_a", "task_b"]` | Waits until both `task_a` and `task_b` complete |
193
+
194
+ ```ruby
195
+ RobotLab.create_network(name: "pipeline", parallel_mode: :ractor) do
196
+ task :ingest, ingester, depends_on: :none
197
+ task :classify, classifier, depends_on: ["ingest"]
198
+ task :summarize, summarizer, depends_on: ["ingest"]
199
+ task :report, reporter, depends_on: ["classify", "summarize"]
200
+ end
201
+ ```
202
+
203
+ ---
204
+
205
+ ## Shared Memory Across Ractors
206
+
207
+ Robots running in Ractor workers cannot share a standard `Memory` instance directly — it contains mutable Ruby objects. RobotLab solves this with `RactorMemoryProxy`, which wraps a `Memory` via `Ractor::Wrapper`.
208
+
209
+ You typically interact with the proxy from the thread side (before and after Ractor dispatch), not from inside workers. Workers receive the frozen result string; the scheduler stores it in `completed` for subsequent waves.
210
+
211
+ For cases where you need Ractor workers to write into shared memory at runtime, use the proxy's Ractor-shareable stub:
212
+
213
+ ```ruby
214
+ memory = RobotLab::Memory.new
215
+ proxy = RobotLab::RactorMemoryProxy.new(memory)
216
+
217
+ # Pass the stub (not the proxy) into Ractor.new
218
+ Ractor.new(proxy.stub) do |mem|
219
+ mem.set(:status, "done")
220
+ mem.get(:status) # => "done"
221
+ end.value
222
+
223
+ memory.get(:status) # => "done"
224
+
225
+ proxy.shutdown
226
+ ```
227
+
228
+ Values written via `set` are automatically deep-frozen before crossing the boundary.
229
+
230
+ ---
231
+
232
+ ## The Frozen-Data Contract
233
+
234
+ Everything that crosses a Ractor boundary must be Ractor-shareable: frozen strings, frozen hashes, frozen arrays, `Data.define` structs, and integers/symbols/nil.
235
+
236
+ `RactorBoundary.freeze_deep` recursively freezes a nested Hash/Array structure and raises `RactorBoundaryError` if it encounters something that cannot be made shareable (like a `StringIO` or a `Proc`):
237
+
238
+ ```ruby
239
+ safe = RobotLab::RactorBoundary.freeze_deep({ key: "value", tags: ["a", "b"] })
240
+ # => { key: "value", tags: ["a", "b"] } (all frozen)
241
+
242
+ RobotLab::RactorBoundary.freeze_deep(StringIO.new)
243
+ # => raises RobotLab::RactorBoundaryError
244
+ ```
245
+
246
+ You generally do not need to call this directly — `RactorWorkerPool#submit` and `RactorMemoryProxy#set` call it for you. But it is public if you build tooling on top.
247
+
248
+ ---
249
+
250
+ ## Error Handling
251
+
252
+ ### Tool Errors
253
+
254
+ If a Ractor-safe tool raises inside a worker, the worker catches the error, wraps it in a `RactorJobError`, and sends it back through the reply queue. The pool unwraps it and re-raises as `RobotLab::ToolError`:
255
+
256
+ ```ruby
257
+ begin
258
+ pool.submit("MyTool", { input: "bad data" })
259
+ rescue RobotLab::ToolError => e
260
+ puts e.message # "Tool 'MyTool' failed in Ractor: ..."
261
+ end
262
+ ```
263
+
264
+ ### Robot Pipeline Errors
265
+
266
+ The scheduler raises `RobotLab::Error` if a robot fails inside a Ractor worker:
267
+
268
+ ```ruby
269
+ begin
270
+ network.run(message: "go")
271
+ rescue RobotLab::Error => e
272
+ puts e.message # "Robot 'summarize' failed in Ractor: ..."
273
+ end
274
+ ```
275
+
276
+ ### Boundary Errors
277
+
278
+ Passing unshareable data raises `RobotLab::RactorBoundaryError` before any Ractor is involved:
279
+
280
+ ```ruby
281
+ begin
282
+ pool.submit("MyTool", { io: StringIO.new })
283
+ rescue RobotLab::RactorBoundaryError => e
284
+ puts e.message # "Cannot make value Ractor-shareable: ..."
285
+ end
286
+ ```
287
+
288
+ ---
289
+
290
+ ## Configuration Reference
291
+
292
+ | Parameter | Where | Default | Description |
293
+ |---|---|---|---|
294
+ | `ractor_pool_size` | `RunConfig` / global config | `Etc.nprocessors` | Worker count for `RactorWorkerPool` |
295
+ | `parallel_mode` | `Network.new` | `:async` | `:async` (SimpleFlow) or `:ractor` (RactorNetworkScheduler) |
296
+
297
+ ---
298
+
299
+ ## Best Practices
300
+
301
+ ### 1. Profile Before Reaching for Ractors
302
+
303
+ Ractors add overhead: freezing data, queue coordination, thread synchronization. For fast tools or networks with few tasks, standard threads are often faster. Measure first.
304
+
305
+ ### 2. Keep Tool State Stateless
306
+
307
+ The safest Ractor-safe tool is a pure function:
308
+
309
+ ```ruby
310
+ class NormalizeText < RubyLLM::Tool
311
+ ractor_safe true
312
+ description "Unicode-normalize and strip a string"
313
+ param :text, type: :string, desc: "Input text"
314
+
315
+ def execute(text:)
316
+ text.unicode_normalize(:nfkc).strip
317
+ end
318
+ end
319
+ ```
320
+
321
+ ### 3. Freeze Tool Return Values
322
+
323
+ Tool results travel back through the reply queue — freeze them proactively to avoid the overhead of `Ractor.make_shareable`:
324
+
325
+ ```ruby
326
+ def execute(id:)
327
+ { id: id, name: "result" }.freeze
328
+ end
329
+ ```
330
+
331
+ ### 4. Parallel Mode Doesn't Share Robot Instances
332
+
333
+ Each Ractor worker constructs a **fresh Robot** from the frozen spec. Side-effects on the original robot objects (callbacks, in-memory state) are not visible inside workers. Use `Memory` (via `RactorMemoryProxy`) for shared state.
334
+
335
+ ### 5. LLM Calls Stay in Threads
336
+
337
+ `ruby_llm` is not Ractor-safe. Workers spawn a Thread internally for each LLM call and block the Ractor fiber on the thread result. This is transparent — you don't need to do anything — but it means robot-mode Ractors are I/O-concurrent, not purely CPU-parallel.
338
+
339
+ ### 6. Shut Down the Pool Cleanly
340
+
341
+ Always shut down the global pool before exiting, especially in scripts:
342
+
343
+ ```ruby
344
+ at_exit { RobotLab.shutdown_ractor_pool }
345
+ ```
346
+
347
+ ---
348
+
349
+ ## Constraints and Limitations
350
+
351
+ - **No closures across boundaries.** Procs and lambdas cannot cross Ractor boundaries. Callbacks (`on_tool_call`, `on_tool_result`) registered on the outer robot are not available inside workers.
352
+ - **No mutable class-level state.** Class variables and module globals accessed from `execute` must be frozen.
353
+ - **`parallel_mode: :ractor` returns a plain Hash**, not a `SimpleFlow::Result`. If downstream code depends on `result.context` or `result.value`, use `:async` mode.
354
+ - **Memory subscriptions don't transfer.** Subscriptions registered on the outer `Memory` before a Ractor dispatch are not triggered by writes made via `RactorMemoryProxy#set` inside workers during the run.
355
+ - **Ruby version.** Ractors require Ruby 3.0+. `Ractor#value` / `Ractor#join` are the supported APIs from Ruby 4.0 onwards (`Ractor#take` was removed).
356
+
357
+ ---
358
+
359
+ ## Next Steps
360
+
361
+ - [Using Tools](using-tools.md) — Tool definitions and configuration
362
+ - [Creating Networks](creating-networks.md) — Network orchestration patterns
363
+ - [Memory System](memory.md) — Shared data between robots
364
+ - [API Reference: Network](../api/core/network.md) — Complete Network API
data/docs/index.md ADDED
@@ -0,0 +1,69 @@
1
+ # robot_lab-ractor
2
+
3
+ Ractor-based CPU parallelism for the [RobotLab](https://github.com/MadBomber/robot_lab) LLM agent framework.
4
+
5
+ > [!CAUTION]
6
+ > This gem is under active development. APIs may change without notice.
7
+
8
+ ## What it provides
9
+
10
+ - **`RactorWorkerPool`** — shared pool of Ractor workers for CPU-bound tools; tools marked `ractor_safe true` are routed through it automatically
11
+ - **`RactorNetworkScheduler`** — DAG-aware parallel execution of robot pipelines using Ractors
12
+ - **`RactorBoundary`** — deep-freeze utilities for safely sharing objects across Ractor boundaries
13
+ - **`RactorMemoryProxy`** — thread-safe proxy exposing `RobotLab::Memory` to Ractor workers via `Ractor::Wrapper`
14
+ - **`RobotLab.ractor_pool` / `.shutdown_ractor_pool`** — process-level pool management added to the `RobotLab` module
15
+
16
+ ## Installation
17
+
18
+ Add to your Gemfile:
19
+
20
+ ```ruby
21
+ gem "robot_lab"
22
+ gem "robot_lab-ractor"
23
+ ```
24
+
25
+ ## Quick Example
26
+
27
+ ```ruby
28
+ require "robot_lab"
29
+ require "robot_lab/ractor"
30
+
31
+ # Mark a tool as Ractor-safe to route it through the pool
32
+ class NumberCruncher < RobotLab::Tool
33
+ description "CPU-intensive calculation"
34
+ param :n, type: "number", desc: "Input"
35
+ ractor_safe true
36
+
37
+ def execute(n:)
38
+ # runs in a Ractor worker, not the main thread
39
+ (1..n.to_i).sum
40
+ end
41
+ end
42
+
43
+ robot = RobotLab.build(
44
+ name: "cruncher",
45
+ system_prompt: "You crunch numbers.",
46
+ local_tools: [NumberCruncher]
47
+ )
48
+
49
+ result = robot.run("Sum all numbers from 1 to 1000.")
50
+ puts result.last_text_content
51
+ ```
52
+
53
+ ## Pool Management
54
+
55
+ ```ruby
56
+ # Pool is lazily created on first use
57
+ pool = RobotLab.ractor_pool
58
+
59
+ # Drain and shut down explicitly (e.g. at process exit)
60
+ RobotLab.shutdown_ractor_pool
61
+ ```
62
+
63
+ ## Links
64
+
65
+ - [Ractor Parallelism Guide](guides/ractor-parallelism.md)
66
+ - [Implementation Plan](superpowers/plans/2026-04-14-ractor-integration.md)
67
+ - [Design Spec](superpowers/specs/2026-04-14-ractor-integration-design.md)
68
+ - [RobotLab Core](https://github.com/MadBomber/robot_lab)
69
+ - [RubyGems](https://rubygems.org/gems/robot_lab-ractor)