robot_lab 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/.architecture/AGENTS.md +32 -0
- data/.architecture/config.yml +8 -0
- data/.architecture/members.yml +60 -0
- data/.architecture/reviews/feature-free-will.md +490 -0
- data/.architecture/reviews/overall-codebase.md +427 -0
- data/.claude/settings.local.json +57 -0
- data/.codex/config.toml +2 -0
- data/.irbrc +2 -2
- data/.rubocop.yml +172 -0
- data/CHANGELOG.md +72 -0
- data/CLAUDE.md +139 -0
- data/README.md +91 -95
- data/Rakefile +109 -3
- data/agent2agent_review.md +192 -0
- data/agentf_improvements.md +253 -0
- data/agents.md +14 -0
- data/docs/examples/index.md +37 -2
- data/docs/getting-started/configuration.md +20 -7
- data/docs/guides/index.md +16 -16
- data/docs/guides/knowledge.md +7 -1
- data/docs/guides/observability.md +132 -0
- data/docs/index.md +30 -3
- data/docs/superpowers/plans/2026-05-06-agentskills.md +1303 -0
- data/docs/superpowers/specs/2026-05-06-agentskills-design.md +247 -0
- data/examples/.envrc +1 -0
- data/examples/01_simple_robot.rb +5 -9
- data/examples/02_tools.rb +5 -9
- data/examples/03_network.rb +8 -9
- data/examples/04_mcp.rb +21 -29
- data/examples/05_streaming.rb +12 -18
- data/examples/06_prompt_templates.rb +11 -19
- data/examples/07_network_memory.rb +16 -31
- data/examples/08_llm_config.rb +10 -22
- data/examples/09_chaining.rb +16 -27
- data/examples/10_memory.rb +12 -28
- data/examples/11_network_introspection.rb +15 -29
- data/examples/12_message_bus.rb +5 -12
- data/examples/13_spawn.rb +5 -10
- data/examples/14_rusty_circuit/.envrc +1 -0
- data/examples/14_rusty_circuit/comic.rb +2 -0
- data/examples/14_rusty_circuit/heckler.rb +1 -1
- data/examples/14_rusty_circuit/open_mic.rb +1 -3
- data/examples/14_rusty_circuit/scout.rb +2 -0
- data/examples/15_memory_network_and_bus/.envrc +1 -0
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +6 -3
- data/examples/15_memory_network_and_bus/linux_writer.rb +1 -1
- data/examples/15_memory_network_and_bus/output/combined_article.md +6 -6
- data/examples/15_memory_network_and_bus/output/final_article.md +6 -8
- data/examples/15_memory_network_and_bus/output/linux_draft.md +4 -2
- data/examples/15_memory_network_and_bus/output/mac_draft.md +3 -3
- data/examples/15_memory_network_and_bus/output/memory.json +6 -6
- data/examples/15_memory_network_and_bus/output/revision_1.md +10 -11
- data/examples/15_memory_network_and_bus/output/revision_2.md +6 -8
- data/examples/15_memory_network_and_bus/output/windows_draft.md +3 -3
- data/examples/16_writers_room/.envrc +1 -0
- data/examples/16_writers_room/writers_room.rb +2 -4
- data/examples/17_skills.rb +8 -17
- data/examples/18_rails/Gemfile +1 -0
- data/examples/19_token_tracking.rb +9 -15
- data/examples/20_circuit_breaker.rb +10 -19
- data/examples/21_learning_loop.rb +11 -20
- data/examples/22_context_compression.rb +6 -13
- data/examples/23_convergence.rb +6 -17
- data/examples/24_structured_delegation.rb +11 -15
- data/examples/25_history_search.rb +5 -12
- data/examples/26_document_store.rb +6 -13
- data/examples/27_incident_response/incident_response.rb +4 -5
- data/examples/28_mcp_discovery.rb +8 -11
- data/examples/29_ractor_tools.rb +4 -9
- data/examples/30_ractor_network.rb +10 -19
- data/examples/31_launch_assessment.rb +10 -23
- data/examples/32_newsletter_reader.rb +188 -0
- data/examples/33_stock_generator.rb +80 -0
- data/examples/33_stock_predictor.rb +306 -0
- data/examples/34_agentskills.rb +72 -0
- data/examples/README.md +1 -1
- data/examples/common.rb +76 -0
- data/examples/ruboruby.md +423 -0
- data/examples/temp.md +51 -0
- data/lib/robot_lab/agent_skill.rb +63 -0
- data/lib/robot_lab/agent_skill_catalog.rb +74 -0
- data/lib/robot_lab/ask_user.rb +2 -2
- data/lib/robot_lab/bus_poller.rb +12 -5
- data/lib/robot_lab/config.rb +1 -12
- data/lib/robot_lab/delegation_future.rb +1 -1
- data/lib/robot_lab/doom_loop_detector.rb +98 -0
- data/lib/robot_lab/history_compressor.rb +4 -10
- data/lib/robot_lab/mcp/client.rb +1 -2
- data/lib/robot_lab/mcp/connection_poller.rb +3 -3
- data/lib/robot_lab/mcp/server.rb +1 -1
- data/lib/robot_lab/mcp/server_discovery.rb +0 -2
- data/lib/robot_lab/memory.rb +32 -27
- data/lib/robot_lab/memory_change.rb +2 -2
- data/lib/robot_lab/message.rb +4 -4
- data/lib/robot_lab/network.rb +11 -6
- data/lib/robot_lab/robot/agent_skill_matching.rb +99 -0
- data/lib/robot_lab/robot/bus_messaging.rb +9 -27
- data/lib/robot_lab/robot/history_search.rb +4 -1
- data/lib/robot_lab/robot/mcp_management.rb +5 -11
- data/lib/robot_lab/robot/template_rendering.rb +60 -40
- data/lib/robot_lab/robot.rb +323 -206
- data/lib/robot_lab/robot_result.rb +6 -5
- data/lib/robot_lab/run_config.rb +5 -11
- data/lib/robot_lab/script_tool.rb +76 -0
- data/lib/robot_lab/state_proxy.rb +7 -5
- data/lib/robot_lab/tool.rb +3 -3
- data/lib/robot_lab/tool_config.rb +1 -1
- data/lib/robot_lab/tool_manifest.rb +5 -7
- data/lib/robot_lab/user_message.rb +2 -2
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab/waiter.rb +1 -1
- data/lib/robot_lab.rb +41 -52
- data/logfile +8 -0
- data/mkdocs.yml +2 -3
- data/robot_concurrency.md +38 -0
- data/simple_acp_review.md +298 -0
- data/site/404.html +2300 -0
- data/site/api/core/index.html +2706 -0
- data/site/api/core/memory/index.html +3793 -0
- data/site/api/core/network/index.html +3500 -0
- data/site/api/core/robot/index.html +4566 -0
- data/site/api/core/state/index.html +3390 -0
- data/site/api/core/tool/index.html +3843 -0
- data/site/api/index.html +2635 -0
- data/site/api/mcp/client/index.html +3435 -0
- data/site/api/mcp/index.html +2783 -0
- data/site/api/mcp/server/index.html +3252 -0
- data/site/api/mcp/transports/index.html +3352 -0
- data/site/api/messages/index.html +2641 -0
- data/site/api/messages/text-message/index.html +3087 -0
- data/site/api/messages/tool-call-message/index.html +3159 -0
- data/site/api/messages/tool-result-message/index.html +3252 -0
- data/site/api/messages/user-message/index.html +3212 -0
- data/site/api/streaming/context/index.html +3282 -0
- data/site/api/streaming/events/index.html +3347 -0
- data/site/api/streaming/index.html +2738 -0
- data/site/architecture/core-concepts/index.html +3757 -0
- data/site/architecture/index.html +2797 -0
- data/site/architecture/message-flow/index.html +3238 -0
- data/site/architecture/network-orchestration/index.html +3433 -0
- data/site/architecture/robot-execution/index.html +3140 -0
- data/site/architecture/state-management/index.html +3498 -0
- data/site/assets/css/custom.css +56 -0
- data/site/assets/images/favicon.png +0 -0
- data/site/assets/images/robot_lab.jpg +0 -0
- data/site/assets/javascripts/bundle.79ae519e.min.js +16 -0
- data/site/assets/javascripts/bundle.79ae519e.min.js.map +7 -0
- data/site/assets/javascripts/lunr/min/lunr.ar.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.da.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.de.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.du.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.el.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.es.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.fi.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.fr.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.he.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.hi.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.hu.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.hy.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.it.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.ja.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.jp.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.kn.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.ko.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.multi.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.nl.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.no.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.pt.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.ro.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.ru.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.sa.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.sv.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.ta.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.te.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.th.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.tr.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.vi.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.zh.min.js +1 -0
- data/site/assets/javascripts/lunr/tinyseg.js +206 -0
- data/site/assets/javascripts/lunr/wordcut.js +6708 -0
- data/site/assets/javascripts/workers/search.2c215733.min.js +42 -0
- data/site/assets/javascripts/workers/search.2c215733.min.js.map +7 -0
- data/site/assets/stylesheets/main.484c7ddc.min.css +1 -0
- data/site/assets/stylesheets/main.484c7ddc.min.css.map +1 -0
- data/site/assets/stylesheets/palette.ab4e12ef.min.css +1 -0
- data/site/assets/stylesheets/palette.ab4e12ef.min.css.map +1 -0
- data/site/concepts/index.html +3455 -0
- data/site/examples/basic-chat/index.html +2880 -0
- data/site/examples/index.html +2907 -0
- data/site/examples/mcp-server/index.html +3018 -0
- data/site/examples/multi-robot-network/index.html +3131 -0
- data/site/examples/rails-application/index.html +3329 -0
- data/site/examples/tool-usage/index.html +3085 -0
- data/site/getting-started/configuration/index.html +3745 -0
- data/site/getting-started/index.html +2572 -0
- data/site/getting-started/installation/index.html +2981 -0
- data/site/getting-started/quick-start/index.html +2942 -0
- data/site/guides/building-robots/index.html +4290 -0
- data/site/guides/creating-networks/index.html +3858 -0
- data/site/guides/index.html +2586 -0
- data/site/guides/mcp-integration/index.html +3581 -0
- data/site/guides/memory/index.html +3586 -0
- data/site/guides/rails-integration/index.html +4019 -0
- data/site/guides/streaming/index.html +3157 -0
- data/site/guides/using-tools/index.html +3802 -0
- data/site/index.html +2671 -0
- data/site/search/search_index.json +1 -0
- data/site/sitemap.xml +183 -0
- data/site/sitemap.xml.gz +0 -0
- data/site/tags.json +1 -0
- data/temp.md +6 -0
- data/tool_manifest_plan.md +155 -0
- metadata +154 -92
- data/docs/examples/rails-application.md +0 -419
- data/docs/guides/ractor-parallelism.md +0 -364
- data/docs/guides/rails-integration.md +0 -681
- data/docs/superpowers/plans/2026-04-14-ractor-integration.md +0 -1538
- data/docs/superpowers/specs/2026-04-14-ractor-integration-design.md +0 -258
- data/lib/generators/robot_lab/install_generator.rb +0 -90
- data/lib/generators/robot_lab/job_generator.rb +0 -40
- data/lib/generators/robot_lab/robot_generator.rb +0 -55
- data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -42
- data/lib/generators/robot_lab/templates/job.rb.tt +0 -21
- data/lib/generators/robot_lab/templates/migration.rb.tt +0 -32
- data/lib/generators/robot_lab/templates/result_model.rb.tt +0 -52
- data/lib/generators/robot_lab/templates/robot.rb.tt +0 -31
- data/lib/generators/robot_lab/templates/robot_job.rb.tt +0 -18
- data/lib/generators/robot_lab/templates/robot_test.rb.tt +0 -34
- data/lib/generators/robot_lab/templates/routing_robot.rb.tt +0 -59
- data/lib/generators/robot_lab/templates/thread_model.rb.tt +0 -40
- data/lib/robot_lab/document_store.rb +0 -155
- data/lib/robot_lab/ractor_boundary.rb +0 -42
- data/lib/robot_lab/ractor_job.rb +0 -37
- data/lib/robot_lab/ractor_memory_proxy.rb +0 -85
- data/lib/robot_lab/ractor_network_scheduler.rb +0 -154
- data/lib/robot_lab/ractor_worker_pool.rb +0 -117
- data/lib/robot_lab/rails_integration/engine.rb +0 -29
- data/lib/robot_lab/rails_integration/job.rb +0 -158
- data/lib/robot_lab/rails_integration/railtie.rb +0 -51
- data/lib/robot_lab/rails_integration/turbo_stream_callbacks.rb +0 -72
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# <%= class_name %> Routing Robot
|
|
4
|
-
#
|
|
5
|
-
# <%= robot_description %>
|
|
6
|
-
#
|
|
7
|
-
# This robot classifies requests and activates optional tasks in a Network.
|
|
8
|
-
# Use it as the first task in a network with optional downstream tasks:
|
|
9
|
-
#
|
|
10
|
-
# classifier = <%= class_name %>Robot.build
|
|
11
|
-
# billing = BillingRobot.build
|
|
12
|
-
# technical = TechnicalRobot.build
|
|
13
|
-
#
|
|
14
|
-
# network = RobotLab.create_network(name: "support") do
|
|
15
|
-
# task :classifier, classifier, depends_on: :none
|
|
16
|
-
# task :billing, billing, depends_on: :optional
|
|
17
|
-
# task :technical, technical, depends_on: :optional
|
|
18
|
-
# end
|
|
19
|
-
#
|
|
20
|
-
# result = network.run(message: "I was charged twice")
|
|
21
|
-
#
|
|
22
|
-
class <%= class_name %>Robot < RobotLab::Robot
|
|
23
|
-
SYSTEM_PROMPT = <<~PROMPT
|
|
24
|
-
You are a routing robot that classifies user requests.
|
|
25
|
-
|
|
26
|
-
Analyze the user's request and respond with ONLY the category name.
|
|
27
|
-
Valid categories: billing, technical, general
|
|
28
|
-
PROMPT
|
|
29
|
-
|
|
30
|
-
def self.build(**options)
|
|
31
|
-
new(
|
|
32
|
-
name: "<%= file_name %>",
|
|
33
|
-
description: "<%= robot_description %>",
|
|
34
|
-
system_prompt: SYSTEM_PROMPT,
|
|
35
|
-
**options
|
|
36
|
-
)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Override call to inspect classification output and activate optional tasks.
|
|
40
|
-
def call(result)
|
|
41
|
-
context = extract_run_context(result)
|
|
42
|
-
message = context.delete(:message)
|
|
43
|
-
|
|
44
|
-
robot_result = run(message, **context)
|
|
45
|
-
|
|
46
|
-
new_result = result
|
|
47
|
-
.with_context(@name.to_sym, robot_result)
|
|
48
|
-
.continue(robot_result)
|
|
49
|
-
|
|
50
|
-
category = robot_result.last_text_content.to_s.strip.downcase
|
|
51
|
-
|
|
52
|
-
# Route based on classification — customize these patterns
|
|
53
|
-
case category
|
|
54
|
-
when /billing/ then new_result.activate(:billing)
|
|
55
|
-
when /technical/ then new_result.activate(:technical)
|
|
56
|
-
else new_result.activate(:general)
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# RobotLab Thread Model
|
|
4
|
-
#
|
|
5
|
-
# Stores conversation threads for history persistence.
|
|
6
|
-
#
|
|
7
|
-
class RobotLabThread < ApplicationRecord
|
|
8
|
-
has_many :results,
|
|
9
|
-
class_name: "RobotLabResult",
|
|
10
|
-
foreign_key: :session_id,
|
|
11
|
-
primary_key: :session_id,
|
|
12
|
-
dependent: :destroy
|
|
13
|
-
|
|
14
|
-
validates :session_id, presence: true, uniqueness: true
|
|
15
|
-
|
|
16
|
-
# Find or create a thread by ID
|
|
17
|
-
#
|
|
18
|
-
# @param id [String] Thread ID
|
|
19
|
-
# @return [RobotLabThread]
|
|
20
|
-
#
|
|
21
|
-
def self.find_or_create_by_session_id(id)
|
|
22
|
-
find_or_create_by(session_id: id)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Get the last result for this thread
|
|
26
|
-
#
|
|
27
|
-
# @return [RobotLabResult, nil]
|
|
28
|
-
#
|
|
29
|
-
def last_result
|
|
30
|
-
results.order(sequence_number: :desc).first
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# Get message count for this thread
|
|
34
|
-
#
|
|
35
|
-
# @return [Integer]
|
|
36
|
-
#
|
|
37
|
-
def message_count
|
|
38
|
-
results.count
|
|
39
|
-
end
|
|
40
|
-
end
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "fastembed"
|
|
4
|
-
|
|
5
|
-
module RobotLab
|
|
6
|
-
# Embedding-based document store for semantic search over arbitrary text.
|
|
7
|
-
#
|
|
8
|
-
# Documents are embedded using {https://github.com/khasinski/fastembed-rb fastembed}
|
|
9
|
-
# (BAAI/bge-small-en-v1.5 by default) and stored in memory. Queries are
|
|
10
|
-
# embedded the same way, then compared by cosine similarity to find the
|
|
11
|
-
# closest documents.
|
|
12
|
-
#
|
|
13
|
-
# The embedding model is initialised lazily on first use — the ONNX model
|
|
14
|
-
# file is downloaded on that first call (cached locally afterwards).
|
|
15
|
-
#
|
|
16
|
-
# @example
|
|
17
|
-
# store = RobotLab::DocumentStore.new
|
|
18
|
-
# store.store(:q4_report, "Q4 revenue came in at $4.2M, up 18% YoY…")
|
|
19
|
-
# store.store(:q3_report, "Q3 showed 15% growth, driven by APAC…")
|
|
20
|
-
#
|
|
21
|
-
# results = store.search("revenue growth", limit: 2)
|
|
22
|
-
# results.each { |r| puts "#{r[:key]} (#{r[:score].round(3)}): #{r[:text][0..60]}" }
|
|
23
|
-
#
|
|
24
|
-
# @example Via Memory
|
|
25
|
-
# memory.store_document(:readme, File.read("README.md"))
|
|
26
|
-
# memory.search_documents("how to configure redis", limit: 3)
|
|
27
|
-
#
|
|
28
|
-
class DocumentStore
|
|
29
|
-
# Default embedding model used when none is specified.
|
|
30
|
-
DEFAULT_MODEL = "BAAI/bge-small-en-v1.5"
|
|
31
|
-
|
|
32
|
-
# @param model_name [String] fastembed model name (default: BAAI/bge-small-en-v1.5)
|
|
33
|
-
def initialize(model_name: DEFAULT_MODEL)
|
|
34
|
-
@model_name = model_name
|
|
35
|
-
@documents = {} # key (Symbol) => { text: String, vector: Array<Float> }
|
|
36
|
-
@mutex = Mutex.new
|
|
37
|
-
@model = nil # lazy: initialised on first embed call
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Embed +text+ and store it under +key+.
|
|
41
|
-
#
|
|
42
|
-
# If a document already exists under +key+ it is replaced.
|
|
43
|
-
#
|
|
44
|
-
# @param key [Symbol, String] identifier for this document
|
|
45
|
-
# @param text [String] the document text to embed and store
|
|
46
|
-
# @return [self]
|
|
47
|
-
def store(key, text)
|
|
48
|
-
key = key.to_sym
|
|
49
|
-
vector = passage_vector(text)
|
|
50
|
-
@mutex.synchronize { @documents[key] = { text: text, vector: vector } }
|
|
51
|
-
self
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Search for documents semantically similar to +query+.
|
|
55
|
-
#
|
|
56
|
-
# @param query [String] natural-language search query
|
|
57
|
-
# @param limit [Integer] maximum number of results (default 5)
|
|
58
|
-
# @return [Array<Hash>] results sorted by score descending.
|
|
59
|
-
# Each hash contains +:key+, +:text+, and +:score+ (Float 0.0..1.0).
|
|
60
|
-
def search(query, limit: 5)
|
|
61
|
-
return [] if empty?
|
|
62
|
-
|
|
63
|
-
query_vec = query_vector(query)
|
|
64
|
-
results = []
|
|
65
|
-
|
|
66
|
-
@mutex.synchronize do
|
|
67
|
-
@documents.each do |key, doc|
|
|
68
|
-
score = cosine_similarity(query_vec, doc[:vector])
|
|
69
|
-
results << { key: key, text: doc[:text], score: score }
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
results.sort_by { |r| -r[:score] }.first(limit)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# Number of stored documents.
|
|
77
|
-
#
|
|
78
|
-
# @return [Integer]
|
|
79
|
-
def size
|
|
80
|
-
@mutex.synchronize { @documents.size }
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Keys of all stored documents.
|
|
84
|
-
#
|
|
85
|
-
# @return [Array<Symbol>]
|
|
86
|
-
def keys
|
|
87
|
-
@mutex.synchronize { @documents.keys }
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# Whether the store contains no documents.
|
|
91
|
-
#
|
|
92
|
-
# @return [Boolean]
|
|
93
|
-
def empty?
|
|
94
|
-
@mutex.synchronize { @documents.empty? }
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Remove the document stored under +key+.
|
|
98
|
-
#
|
|
99
|
-
# @param key [Symbol, String]
|
|
100
|
-
# @return [self]
|
|
101
|
-
def delete(key)
|
|
102
|
-
@mutex.synchronize { @documents.delete(key.to_sym) }
|
|
103
|
-
self
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Remove all stored documents.
|
|
107
|
-
#
|
|
108
|
-
# @return [self]
|
|
109
|
-
def clear
|
|
110
|
-
@mutex.synchronize { @documents.clear }
|
|
111
|
-
self
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
private
|
|
115
|
-
|
|
116
|
-
# Return (or lazily create) the fastembed model instance.
|
|
117
|
-
def model
|
|
118
|
-
@model ||= Fastembed::TextEmbedding.new(model_name: @model_name, show_progress: false)
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
# Embed a single passage string.
|
|
122
|
-
def passage_vector(text)
|
|
123
|
-
model.passage_embed([text]).to_a.first
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
# Embed a single query string.
|
|
127
|
-
def query_vector(text)
|
|
128
|
-
model.query_embed([text]).to_a.first
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
# Cosine similarity between two dense float vectors.
|
|
132
|
-
#
|
|
133
|
-
# Returns 0.0 for nil, empty, or mismatched-length vectors.
|
|
134
|
-
def cosine_similarity(vec_a, vec_b)
|
|
135
|
-
return 0.0 unless vec_a && vec_b
|
|
136
|
-
return 0.0 if vec_a.empty? || vec_b.empty?
|
|
137
|
-
return 0.0 if vec_a.length != vec_b.length
|
|
138
|
-
|
|
139
|
-
dot = 0.0
|
|
140
|
-
norm_a = 0.0
|
|
141
|
-
norm_b = 0.0
|
|
142
|
-
|
|
143
|
-
vec_a.each_with_index do |a, i|
|
|
144
|
-
b = vec_b[i]
|
|
145
|
-
dot += a * b
|
|
146
|
-
norm_a += a * a
|
|
147
|
-
norm_b += b * b
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
return 0.0 if norm_a.zero? || norm_b.zero?
|
|
151
|
-
|
|
152
|
-
dot / (Math.sqrt(norm_a) * Math.sqrt(norm_b))
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
end
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RobotLab
|
|
4
|
-
# Utility for making values safe to pass across Ractor boundaries.
|
|
5
|
-
#
|
|
6
|
-
# Recursively freezes Hash and Array structures. Raises RactorBoundaryError
|
|
7
|
-
# if a value cannot be made Ractor-shareable (e.g. a live IO or Proc).
|
|
8
|
-
#
|
|
9
|
-
# @example
|
|
10
|
-
# safe = RactorBoundary.freeze_deep({ model: "sonnet", args: { x: 1 } })
|
|
11
|
-
# Ractor.shareable?(safe) #=> true
|
|
12
|
-
#
|
|
13
|
-
module RactorBoundary
|
|
14
|
-
# Recursively freeze an object for safe Ractor boundary crossing.
|
|
15
|
-
#
|
|
16
|
-
# @param obj [Object] the value to freeze
|
|
17
|
-
# @return [Object] a frozen, Ractor-shareable copy
|
|
18
|
-
# @raise [RactorBoundaryError] if the value cannot be made shareable
|
|
19
|
-
def self.freeze_deep(obj)
|
|
20
|
-
return obj if Ractor.shareable?(obj)
|
|
21
|
-
|
|
22
|
-
result = case obj
|
|
23
|
-
when Hash
|
|
24
|
-
obj.transform_keys { |k| freeze_deep(k) }
|
|
25
|
-
.transform_values { |v| freeze_deep(v) }
|
|
26
|
-
when Array
|
|
27
|
-
obj.map { |v| freeze_deep(v) }
|
|
28
|
-
else
|
|
29
|
-
begin
|
|
30
|
-
obj.dup
|
|
31
|
-
rescue TypeError
|
|
32
|
-
raise RactorBoundaryError,
|
|
33
|
-
"Cannot make #{obj.class} Ractor-shareable: dup not supported"
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
Ractor.make_shareable(result)
|
|
38
|
-
rescue Ractor::IsolationError, Ractor::Error => e
|
|
39
|
-
raise RactorBoundaryError, "Cannot make value Ractor-shareable: #{e.message}"
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
data/lib/robot_lab/ractor_job.rb
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RobotLab
|
|
4
|
-
# Carrier for work crossing a Ractor boundary.
|
|
5
|
-
#
|
|
6
|
-
# All fields must be Ractor-shareable (frozen Data, frozen String,
|
|
7
|
-
# frozen Hash, or a RactorQueue). Build with RactorBoundary.freeze_deep
|
|
8
|
-
# on the payload before constructing.
|
|
9
|
-
#
|
|
10
|
-
# @example
|
|
11
|
-
# job = RactorJob.new(
|
|
12
|
-
# id: SecureRandom.uuid.freeze,
|
|
13
|
-
# type: :tool,
|
|
14
|
-
# payload: RactorBoundary.freeze_deep({ tool_class: "MyTool", args: { x: 1 } }),
|
|
15
|
-
# reply_queue: RactorQueue.new(capacity: 1)
|
|
16
|
-
# )
|
|
17
|
-
RactorJob = Data.define(:id, :type, :payload, :reply_queue)
|
|
18
|
-
|
|
19
|
-
# Frozen error representation for exceptions raised inside a Ractor worker.
|
|
20
|
-
# Serialized at the Ractor boundary and re-raised on the thread side.
|
|
21
|
-
#
|
|
22
|
-
# @example
|
|
23
|
-
# err = RactorJobError.new(message: e.message.freeze, backtrace: e.backtrace.freeze)
|
|
24
|
-
RactorJobError = Data.define(:message, :backtrace)
|
|
25
|
-
|
|
26
|
-
# Carries everything needed to reconstruct a Robot inside a Ractor.
|
|
27
|
-
# All fields must be frozen strings, symbols, or hashes.
|
|
28
|
-
#
|
|
29
|
-
# @example
|
|
30
|
-
# spec = RobotSpec.new(
|
|
31
|
-
# name: "analyst",
|
|
32
|
-
# template: :analyst,
|
|
33
|
-
# system_prompt: nil,
|
|
34
|
-
# config_hash: { model: "claude-sonnet-4" }.freeze
|
|
35
|
-
# )
|
|
36
|
-
RobotSpec = Data.define(:name, :template, :system_prompt, :config_hash)
|
|
37
|
-
end
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "ractor/wrapper"
|
|
4
|
-
|
|
5
|
-
module RobotLab
|
|
6
|
-
# Wraps a Memory instance via Ractor::Wrapper so Ractor workers can safely
|
|
7
|
-
# read and write shared state.
|
|
8
|
-
#
|
|
9
|
-
# Only get, set, and keys are proxied across the Ractor boundary.
|
|
10
|
-
# Subscriptions and callbacks are NOT proxied — closures are not
|
|
11
|
-
# Ractor-safe. Use the thread-side Memory directly for reactive subscriptions.
|
|
12
|
-
#
|
|
13
|
-
# Values passed to set() must be Ractor-shareable; RactorBoundary.freeze_deep
|
|
14
|
-
# is applied automatically.
|
|
15
|
-
#
|
|
16
|
-
# The proxy uses use_current_ractor: true so the Memory object stays in the
|
|
17
|
-
# calling Ractor and is not moved. This allows direct access alongside the
|
|
18
|
-
# proxy and works with Memory's mutex-based internals.
|
|
19
|
-
#
|
|
20
|
-
# @example
|
|
21
|
-
# memory = Memory.new
|
|
22
|
-
# proxy = RactorMemoryProxy.new(memory)
|
|
23
|
-
#
|
|
24
|
-
# # From any Ractor via the stub:
|
|
25
|
-
# proxy.set(:result, "done")
|
|
26
|
-
# proxy.get(:result) #=> "done"
|
|
27
|
-
#
|
|
28
|
-
# proxy.shutdown # call when done
|
|
29
|
-
#
|
|
30
|
-
class RactorMemoryProxy
|
|
31
|
-
# @param memory [Memory] the memory instance to wrap
|
|
32
|
-
def initialize(memory)
|
|
33
|
-
@memory = memory
|
|
34
|
-
@wrapper = Ractor::Wrapper.new(memory, use_current_ractor: true)
|
|
35
|
-
@stub = @wrapper.stub
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Returns the Ractor-shareable stub for use inside Ractors.
|
|
39
|
-
#
|
|
40
|
-
# The stub proxies get/set/keys to the wrapped Memory. Pass this to
|
|
41
|
-
# Ractor.new rather than the proxy itself (the proxy is not shareable).
|
|
42
|
-
#
|
|
43
|
-
# @return [Ractor::Wrapper stub]
|
|
44
|
-
def stub
|
|
45
|
-
@stub
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Read a value from the proxied Memory.
|
|
49
|
-
#
|
|
50
|
-
# @param key [Symbol]
|
|
51
|
-
# @return [Object, nil]
|
|
52
|
-
def get(key)
|
|
53
|
-
@stub.get(key)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# Write a frozen value to the proxied Memory.
|
|
57
|
-
# The value is deep-frozen before crossing the Ractor boundary.
|
|
58
|
-
#
|
|
59
|
-
# @param key [Symbol]
|
|
60
|
-
# @param value [Object] must be Ractor-shareable after freeze_deep
|
|
61
|
-
# @return [void]
|
|
62
|
-
# @raise [RactorBoundaryError] if value cannot be made shareable
|
|
63
|
-
def set(key, value)
|
|
64
|
-
frozen_value = RactorBoundary.freeze_deep(value)
|
|
65
|
-
@stub.set(key, frozen_value)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# List all keys currently set in the proxied Memory.
|
|
69
|
-
#
|
|
70
|
-
# @return [Array<Symbol>]
|
|
71
|
-
def keys
|
|
72
|
-
@stub.keys
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# Shut down the ractor-wrapper.
|
|
76
|
-
#
|
|
77
|
-
# @return [void]
|
|
78
|
-
def shutdown
|
|
79
|
-
@wrapper.async_stop
|
|
80
|
-
@wrapper.join
|
|
81
|
-
rescue => e
|
|
82
|
-
RobotLab.config.logger.warn("RactorMemoryProxy shutdown error: #{e.message}")
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "etc"
|
|
4
|
-
require "ractor_queue"
|
|
5
|
-
|
|
6
|
-
module RobotLab
|
|
7
|
-
# Schedules frozen robot task descriptions across Ractor workers.
|
|
8
|
-
#
|
|
9
|
-
# Robots stay in threads for LLM calls (ruby_llm is not Ractor-safe).
|
|
10
|
-
# The scheduler distributes frozen RobotSpec payloads; each worker
|
|
11
|
-
# constructs a fresh Robot, runs the task, and returns a frozen result.
|
|
12
|
-
#
|
|
13
|
-
# Task ordering respects depends_on: tasks are only dispatched once all
|
|
14
|
-
# named dependencies have resolved (same topological semantics as
|
|
15
|
-
# SimpleFlow::Pipeline).
|
|
16
|
-
#
|
|
17
|
-
# @example
|
|
18
|
-
# scheduler = RactorNetworkScheduler.new(memory: shared_memory)
|
|
19
|
-
# scheduler.run_pipeline([
|
|
20
|
-
# { spec: analyst_spec, depends_on: :none },
|
|
21
|
-
# { spec: writer_spec, depends_on: ["analyst"] }
|
|
22
|
-
# ], message: "Process this")
|
|
23
|
-
# scheduler.shutdown
|
|
24
|
-
#
|
|
25
|
-
class RactorNetworkScheduler
|
|
26
|
-
# Capacity for the work queue.
|
|
27
|
-
QUEUE_CAPACITY = 256
|
|
28
|
-
|
|
29
|
-
# @param memory [Memory] shared network memory for all robot tasks
|
|
30
|
-
# @param pool_size [Integer, :auto] number of Ractor workers
|
|
31
|
-
def initialize(memory:, pool_size: :auto)
|
|
32
|
-
@memory = memory
|
|
33
|
-
@work_q = RactorQueue.new(capacity: QUEUE_CAPACITY)
|
|
34
|
-
@size = pool_size == :auto ? Etc.nprocessors : pool_size.to_i
|
|
35
|
-
@workers = @size.times.map { spawn_worker(@work_q) }
|
|
36
|
-
@closed = false
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Run a single spec and return the result string.
|
|
40
|
-
#
|
|
41
|
-
# @param spec [RobotSpec]
|
|
42
|
-
# @param message [String]
|
|
43
|
-
# @return [String] the robot's last_text_content
|
|
44
|
-
def run_spec(spec, message:)
|
|
45
|
-
execute_spec(spec, message)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Run a pipeline of specs in dependency order.
|
|
49
|
-
#
|
|
50
|
-
# @param specs_with_deps [Array<Hash>] each entry has :spec and :depends_on
|
|
51
|
-
# :depends_on is :none, :optional, or an Array<String> of spec names
|
|
52
|
-
# @param message [String] initial message passed to entry-point robots
|
|
53
|
-
# @return [Hash<String, String>] name => result for each completed robot
|
|
54
|
-
def run_pipeline(specs_with_deps, message:)
|
|
55
|
-
completed = {} # name => result string
|
|
56
|
-
remaining = specs_with_deps.dup
|
|
57
|
-
|
|
58
|
-
until remaining.empty?
|
|
59
|
-
ready, remaining = remaining.partition do |entry|
|
|
60
|
-
deps = entry[:depends_on]
|
|
61
|
-
deps == :none || deps == :optional ||
|
|
62
|
-
Array(deps).all? { |d| completed.key?(d) }
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
raise RobotLab::Error, "Circular dependency or unresolvable deps in RactorNetworkScheduler" if ready.empty?
|
|
66
|
-
|
|
67
|
-
# Submit all ready tasks concurrently via threads.
|
|
68
|
-
# report_on_exception is disabled because exceptions are propagated
|
|
69
|
-
# to the caller via t.value — the default reporting is redundant noise.
|
|
70
|
-
threads = ready.map do |entry|
|
|
71
|
-
spec = entry[:spec]
|
|
72
|
-
msg = completed.values.last || message
|
|
73
|
-
Thread.new { [spec.name, execute_spec(spec, msg)] }.tap { |t| t.report_on_exception = false }
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
threads.each do |t|
|
|
77
|
-
name, result = t.value
|
|
78
|
-
completed[name] = result
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
completed
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Gracefully shut down worker Ractors.
|
|
86
|
-
# @return [void]
|
|
87
|
-
def shutdown
|
|
88
|
-
return if @closed
|
|
89
|
-
|
|
90
|
-
@closed = true
|
|
91
|
-
@size.times { @work_q.push(nil) }
|
|
92
|
-
@workers.each { |w| w.join rescue nil }
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
private
|
|
96
|
-
|
|
97
|
-
# Dispatch a spec to a Ractor worker and block for the result.
|
|
98
|
-
def execute_spec(spec, message)
|
|
99
|
-
frozen_spec = Ractor.make_shareable(spec)
|
|
100
|
-
frozen_message = message.to_s.freeze
|
|
101
|
-
reply_q = RactorQueue.new(capacity: 1)
|
|
102
|
-
|
|
103
|
-
job = RactorJob.new(
|
|
104
|
-
id: SecureRandom.uuid.freeze,
|
|
105
|
-
type: :robot,
|
|
106
|
-
payload: RactorBoundary.freeze_deep({
|
|
107
|
-
spec: frozen_spec,
|
|
108
|
-
message: frozen_message
|
|
109
|
-
}),
|
|
110
|
-
reply_queue: reply_q
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
@work_q.push(job)
|
|
114
|
-
result = reply_q.pop
|
|
115
|
-
|
|
116
|
-
if result.is_a?(RactorJobError)
|
|
117
|
-
raise RobotLab::Error, "Robot '#{spec.name}' failed in Ractor: #{result.message}"
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
result
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def spawn_worker(work_q)
|
|
124
|
-
Ractor.new(work_q) do |q|
|
|
125
|
-
loop do
|
|
126
|
-
job = q.pop
|
|
127
|
-
break if job.nil?
|
|
128
|
-
|
|
129
|
-
begin
|
|
130
|
-
spec = job.payload[:spec]
|
|
131
|
-
message = job.payload[:message]
|
|
132
|
-
|
|
133
|
-
robot = RobotLab::Robot.new(
|
|
134
|
-
name: spec.name,
|
|
135
|
-
template: spec.template ? spec.template.to_sym : nil,
|
|
136
|
-
system_prompt: spec.system_prompt,
|
|
137
|
-
config: spec.config_hash.empty? ? nil : RobotLab::RunConfig.new(**spec.config_hash.transform_keys(&:to_sym))
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
robot_result = robot.run(message)
|
|
141
|
-
frozen_reply = robot_result.last_text_content.to_s.freeze
|
|
142
|
-
job.reply_queue.push(frozen_reply)
|
|
143
|
-
rescue => e
|
|
144
|
-
err = RobotLab::RactorJobError.new(
|
|
145
|
-
message: e.message.freeze,
|
|
146
|
-
backtrace: (e.backtrace || []).map(&:freeze).freeze
|
|
147
|
-
)
|
|
148
|
-
job.reply_queue.push(err)
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
end
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "etc"
|
|
4
|
-
require "ractor_queue"
|
|
5
|
-
|
|
6
|
-
module RobotLab
|
|
7
|
-
# A pool of Ractor workers that execute CPU-bound, Ractor-safe tools.
|
|
8
|
-
#
|
|
9
|
-
# Work is distributed via a shared RactorQueue. Each worker runs a
|
|
10
|
-
# blocking loop, pops RactorJob instances, dispatches to the named
|
|
11
|
-
# tool class, and pushes the frozen result (or a RactorJobError) to
|
|
12
|
-
# the job's per-job reply_queue.
|
|
13
|
-
#
|
|
14
|
-
# Shutdown uses a poison-pill pattern: one nil sentinel per worker is
|
|
15
|
-
# pushed to the work queue; each worker exits when it pops nil.
|
|
16
|
-
#
|
|
17
|
-
# Only tools that declare +ractor_safe true+ should be submitted.
|
|
18
|
-
# Tool classes are instantiated fresh inside the Ractor for each call.
|
|
19
|
-
#
|
|
20
|
-
# @example
|
|
21
|
-
# pool = RactorWorkerPool.new(size: 4)
|
|
22
|
-
# result = pool.submit("MyTool", { "arg" => "value" })
|
|
23
|
-
# pool.shutdown
|
|
24
|
-
#
|
|
25
|
-
class RactorWorkerPool
|
|
26
|
-
# Capacity of the shared work queue.
|
|
27
|
-
QUEUE_CAPACITY = 1024
|
|
28
|
-
|
|
29
|
-
# @return [Integer] number of worker Ractors
|
|
30
|
-
attr_reader :size
|
|
31
|
-
|
|
32
|
-
# Creates a new pool and starts worker Ractors immediately.
|
|
33
|
-
#
|
|
34
|
-
# @param size [Integer, :auto] number of workers (:auto = Etc.nprocessors)
|
|
35
|
-
def initialize(size: :auto)
|
|
36
|
-
@size = size == :auto ? Etc.nprocessors : size.to_i
|
|
37
|
-
@closed = false
|
|
38
|
-
@work_q = RactorQueue.new(capacity: QUEUE_CAPACITY)
|
|
39
|
-
@workers = @size.times.map { spawn_worker(@work_q) }
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Submit a tool job and block until the result is available.
|
|
43
|
-
#
|
|
44
|
-
# @param tool_class_name [String] fully-qualified Ruby constant name of the tool class
|
|
45
|
-
# @param args [Hash] tool arguments (deep-frozen before crossing Ractor boundary)
|
|
46
|
-
# @return [Object] the tool's return value
|
|
47
|
-
# @raise [RactorBoundaryError] if args cannot be made Ractor-shareable
|
|
48
|
-
# @raise [ToolError] if the tool raises inside the Ractor
|
|
49
|
-
def submit(tool_class_name, args)
|
|
50
|
-
raise ToolError, "Pool is shut down" if @closed
|
|
51
|
-
|
|
52
|
-
reply_q = RactorQueue.new(capacity: 1)
|
|
53
|
-
payload = RactorBoundary.freeze_deep({
|
|
54
|
-
tool_class: tool_class_name.to_s,
|
|
55
|
-
args: args
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
job = RactorJob.new(
|
|
59
|
-
id: SecureRandom.uuid.freeze,
|
|
60
|
-
type: :tool,
|
|
61
|
-
payload: payload,
|
|
62
|
-
reply_queue: reply_q
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
@work_q.push(job)
|
|
66
|
-
result = reply_q.pop
|
|
67
|
-
|
|
68
|
-
if result.is_a?(RactorJobError)
|
|
69
|
-
raise ToolError, "Tool '#{tool_class_name}' failed in Ractor: #{result.message}"
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
result
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# Gracefully shut down the pool.
|
|
76
|
-
#
|
|
77
|
-
# Pushes one nil poison pill per worker so each exits its loop.
|
|
78
|
-
# Waits for all workers to terminate.
|
|
79
|
-
#
|
|
80
|
-
# @return [void]
|
|
81
|
-
def shutdown
|
|
82
|
-
return if @closed
|
|
83
|
-
|
|
84
|
-
@closed = true
|
|
85
|
-
# Push one nil poison pill per worker
|
|
86
|
-
@size.times { @work_q.push(nil) }
|
|
87
|
-
@workers.each { |w| w.join rescue nil }
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
private
|
|
91
|
-
|
|
92
|
-
def spawn_worker(work_q)
|
|
93
|
-
Ractor.new(work_q) do |q|
|
|
94
|
-
loop do
|
|
95
|
-
job = q.pop
|
|
96
|
-
|
|
97
|
-
# nil is the poison pill — exit cleanly
|
|
98
|
-
break if job.nil?
|
|
99
|
-
|
|
100
|
-
begin
|
|
101
|
-
tool_class = Object.const_get(job.payload[:tool_class])
|
|
102
|
-
tool = tool_class.new
|
|
103
|
-
result = tool.execute(**job.payload[:args].transform_keys(&:to_sym))
|
|
104
|
-
frozen_result = Ractor.make_shareable(result.frozen? ? result : result.dup.freeze)
|
|
105
|
-
job.reply_queue.push(frozen_result)
|
|
106
|
-
rescue => e
|
|
107
|
-
err = RobotLab::RactorJobError.new(
|
|
108
|
-
message: e.message.freeze,
|
|
109
|
-
backtrace: (e.backtrace || []).map(&:freeze).freeze
|
|
110
|
-
)
|
|
111
|
-
job.reply_queue.push(err)
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|