robot_lab 0.2.6 → 0.2.8
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 +4 -0
- data/.loki +5 -0
- data/Archspec.rb +44 -0
- data/CHANGELOG.md +17 -1
- data/README.md +124 -64
- data/Rakefile +6 -111
- data/_typos.toml +21 -0
- data/docs/api/core/index.md +41 -15
- data/docs/api/core/memory.md +247 -29
- data/docs/api/core/network.md +255 -33
- data/docs/api/core/result.md +120 -32
- data/docs/api/core/robot.md +551 -61
- data/docs/api/core/state.md +87 -197
- data/docs/api/core/tool.md +165 -20
- data/docs/api/errors.md +110 -17
- data/docs/api/hooks.md +469 -0
- data/docs/api/index.md +80 -7
- data/docs/api/mcp/client.md +129 -35
- data/docs/api/mcp/index.md +164 -23
- data/docs/api/mcp/server.md +27 -3
- data/docs/api/mcp/transports.md +94 -22
- data/docs/api/messages/index.md +26 -3
- data/docs/api/messages/text-message.md +33 -11
- data/docs/api/messages/tool-call-message.md +27 -4
- data/docs/api/messages/tool-result-message.md +23 -4
- data/docs/api/messages/user-message.md +45 -8
- data/docs/api/skills.md +423 -0
- data/docs/api/streaming/context.md +28 -5
- data/docs/api/streaming/index.md +57 -11
- data/docs/api/support.md +846 -0
- data/docs/architecture/core-concepts.md +79 -31
- data/docs/architecture/index.md +87 -11
- data/docs/architecture/message-flow.md +66 -29
- data/docs/architecture/network-orchestration.md +145 -38
- data/docs/architecture/robot-execution.md +172 -90
- data/docs/architecture/state-management.md +31 -12
- data/docs/concepts.md +176 -21
- data/docs/examples/basic-chat.md +72 -19
- data/docs/examples/index.md +117 -31
- data/docs/examples/mcp-server.md +154 -45
- data/docs/examples/multi-robot-network.md +91 -21
- data/docs/examples/tool-usage.md +104 -37
- data/docs/getting-started/configuration.md +288 -98
- data/docs/getting-started/installation.md +53 -41
- data/docs/getting-started/quick-start.md +51 -6
- data/docs/guides/building-robots.md +258 -50
- data/docs/guides/creating-networks.md +214 -30
- data/docs/guides/hooks.md +263 -54
- data/docs/guides/knowledge.md +35 -4
- data/docs/guides/mcp-integration.md +211 -44
- data/docs/guides/memory.md +103 -12
- data/docs/guides/observability.md +95 -47
- data/docs/guides/streaming.md +184 -125
- data/docs/guides/using-tools.md +247 -18
- data/docs/index.md +36 -4
- data/examples/01_simple_robot.rb +2 -2
- data/examples/02_tools.rb +14 -4
- data/examples/03_network.rb +12 -7
- data/examples/04_mcp.rb +11 -4
- data/examples/05_streaming.rb +8 -5
- data/examples/06_prompt_templates.rb +13 -9
- data/examples/07_network_memory.rb +5 -5
- data/examples/08_llm_config.rb +20 -15
- data/examples/09_chaining.rb +4 -4
- data/examples/11_network_introspection.rb +4 -4
- data/examples/12_message_bus.rb +2 -2
- data/examples/13_spawn.rb +2 -2
- data/examples/14_rusty_circuit/README.md +1 -0
- data/examples/14_rusty_circuit/comic.rb +7 -3
- data/examples/14_rusty_circuit/display.rb +14 -0
- data/examples/14_rusty_circuit/heckler.rb +8 -6
- data/examples/14_rusty_circuit/open_mic.rb +17 -6
- data/examples/14_rusty_circuit/scout.rb +17 -10
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +14 -10
- data/examples/15_memory_network_and_bus/linux_writer.rb +2 -2
- data/examples/15_memory_network_and_bus/os_editor.rb +3 -1
- data/examples/15_memory_network_and_bus/os_writer.rb +4 -1
- data/examples/16_writers_room/writer.rb +22 -22
- data/examples/16_writers_room/writers_room.rb +2 -0
- data/examples/17_skills.rb +14 -13
- data/examples/18_rails/README.md +20 -1
- data/examples/18_rails/app/controllers/chat_controller.rb +5 -1
- data/examples/18_rails/app/jobs/robot_run_job.rb +11 -5
- data/examples/18_rails/app/robots/chat_robot.rb +11 -0
- data/examples/18_rails/config/initializers/robot_lab.rb +8 -0
- data/examples/19_token_tracking.rb +25 -9
- data/examples/20_circuit_breaker.rb +10 -7
- data/examples/21_learning_loop.rb +42 -16
- data/examples/22_context_compression.rb +23 -23
- data/examples/23_convergence.rb +24 -17
- data/examples/24_structured_delegation.rb +13 -8
- data/examples/25_history_search.rb +12 -8
- data/examples/27_incident_response/incident_response.rb +31 -13
- data/examples/28_mcp_discovery.rb +17 -13
- data/examples/29_ractor_tools.rb +4 -2
- data/examples/30_ractor_network.rb +22 -17
- data/examples/31_launch_assessment.rb +20 -9
- data/examples/32_newsletter_reader.rb +7 -2
- data/examples/33_stock_predictor.rb +34 -13
- data/examples/34_agentskills.rb +7 -3
- data/examples/35_hooks.rb +18 -8
- data/examples/README.md +199 -45
- data/examples/common.rb +79 -11
- data/examples/xyzzy.rb +8 -1
- data/lib/robot_lab/agent_skill_catalog.rb +1 -0
- data/lib/robot_lab/ask_user.rb +2 -0
- data/lib/robot_lab/bus_poller.rb +2 -0
- data/lib/robot_lab/capabilities.rb +4 -0
- data/lib/robot_lab/config.rb +18 -5
- data/lib/robot_lab/doom_loop_detector.rb +6 -3
- data/lib/robot_lab/history_compressor.rb +5 -0
- data/lib/robot_lab/hook.rb +1 -0
- data/lib/robot_lab/hook_context.rb +4 -0
- data/lib/robot_lab/hook_registry.rb +1 -0
- data/lib/robot_lab/hooks.rb +6 -3
- data/lib/robot_lab/mcp/client.rb +2 -2
- data/lib/robot_lab/mcp/connection_poller.rb +16 -8
- data/lib/robot_lab/mcp/server_discovery.rb +1 -0
- data/lib/robot_lab/mcp/transports/sse.rb +3 -0
- data/lib/robot_lab/mcp/transports/stdio.rb +5 -0
- data/lib/robot_lab/mcp/transports/streamable_http.rb +4 -0
- data/lib/robot_lab/mcp/transports/websocket.rb +3 -0
- data/lib/robot_lab/memory.rb +23 -6
- data/lib/robot_lab/memory_change.rb +1 -0
- data/lib/robot_lab/message.rb +3 -0
- data/lib/robot_lab/names.rb +400 -0
- data/lib/robot_lab/network.rb +13 -6
- data/lib/robot_lab/robot/agent_skill_matching.rb +3 -3
- data/lib/robot_lab/robot/bus_messaging.rb +19 -8
- data/lib/robot_lab/robot/history_search.rb +2 -0
- data/lib/robot_lab/robot/hooking.rb +3 -0
- data/lib/robot_lab/robot/mcp_management.rb +11 -3
- data/lib/robot_lab/robot/template_rendering.rb +29 -7
- data/lib/robot_lab/robot.rb +81 -19
- data/lib/robot_lab/robot_result.rb +3 -0
- data/lib/robot_lab/run_config.rb +5 -0
- data/lib/robot_lab/script_tool.rb +20 -39
- data/lib/robot_lab/state_proxy.rb +1 -0
- data/lib/robot_lab/streaming/context.rb +1 -0
- data/lib/robot_lab/streaming/events.rb +1 -0
- data/lib/robot_lab/task.rb +2 -0
- data/lib/robot_lab/tool.rb +4 -0
- data/lib/robot_lab/user_message.rb +1 -0
- data/lib/robot_lab/utils.rb +2 -0
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab/waiter.rb +3 -0
- data/lib/robot_lab.rb +27 -18
- data/mkdocs.yml +6 -1
- metadata +12 -7
- data/lib/robot_lab/sandbox/null.rb +0 -13
- data/lib/robot_lab/sandbox/seatbelt.rb +0 -104
- data/lib/robot_lab/sandbox.rb +0 -52
data/lib/robot_lab/config.rb
CHANGED
|
@@ -8,7 +8,8 @@ module RobotLab
|
|
|
8
8
|
# Provides:
|
|
9
9
|
# - Nested configuration with a dedicated `ruby_llm:` section
|
|
10
10
|
# - Environment-specific settings (development, test, production)
|
|
11
|
-
# - XDG config file loading (~/.config/robot_lab/
|
|
11
|
+
# - XDG config file loading (~/.config/robot_lab/robot_lab.yml — the filename
|
|
12
|
+
# repeats the `config_name`; `config.yml` is never read)
|
|
12
13
|
# - Environment variable overrides (ROBOT_LAB_*)
|
|
13
14
|
# - Automatic RubyLLM configuration application
|
|
14
15
|
#
|
|
@@ -21,10 +22,14 @@ module RobotLab
|
|
|
21
22
|
# # ROBOT_LAB_RUBY_LLM__MODEL=gpt-4
|
|
22
23
|
# # ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...
|
|
23
24
|
#
|
|
24
|
-
# @example User config file (~/.config/robot_lab/
|
|
25
|
-
# defaults
|
|
26
|
-
#
|
|
27
|
-
#
|
|
25
|
+
# @example User config file (~/.config/robot_lab/robot_lab.yml)
|
|
26
|
+
# # Flat keys, or a section named for the current environment. A `defaults:`
|
|
27
|
+
# # wrapper is IGNORED here — it applies only to the gem's bundled
|
|
28
|
+
# # defaults.yml. This file is NOT run through ERB, so keep secrets in
|
|
29
|
+
# # environment variables or in ./config/robot_lab.yml (which is).
|
|
30
|
+
# ruby_llm:
|
|
31
|
+
# model: claude-sonnet-4
|
|
32
|
+
# request_timeout: 120
|
|
28
33
|
#
|
|
29
34
|
class Config < MywayConfig::Base
|
|
30
35
|
config_name :robot_lab
|
|
@@ -73,6 +78,8 @@ module RobotLab
|
|
|
73
78
|
|
|
74
79
|
private
|
|
75
80
|
|
|
81
|
+
# :reek:UncommunicativeParameterName -- `c` is the RubyLLM config being configured; conventional here (RuboCop allows it).
|
|
82
|
+
# :reek:TooManyStatements -- one set_if_present line per supported provider credential; a loop would obscure the env-var mapping.
|
|
76
83
|
def apply_provider_api_keys(c)
|
|
77
84
|
# Fall back to standard provider env vars when not set in config.
|
|
78
85
|
# This lets users set ANTHROPIC_API_KEY (etc.) directly without
|
|
@@ -98,6 +105,7 @@ module RobotLab
|
|
|
98
105
|
set_if_present(c, :vertexai_location, :vertexai_location, 'GOOGLE_CLOUD_LOCATION')
|
|
99
106
|
end
|
|
100
107
|
|
|
108
|
+
# :reek:UncommunicativeParameterName -- `c` is the RubyLLM config being configured; conventional here.
|
|
101
109
|
def apply_provider_endpoints(c)
|
|
102
110
|
c.openai_api_base = ruby_llm.openai_api_base if ruby_llm.openai_api_base
|
|
103
111
|
c.gemini_api_base = ruby_llm.gemini_api_base if ruby_llm.gemini_api_base
|
|
@@ -106,12 +114,14 @@ module RobotLab
|
|
|
106
114
|
c.xai_api_base = ruby_llm.xai_api_base if ruby_llm.xai_api_base
|
|
107
115
|
end
|
|
108
116
|
|
|
117
|
+
# :reek:UncommunicativeParameterName -- `c` is the RubyLLM config being configured; conventional here.
|
|
109
118
|
def apply_openai_options(c)
|
|
110
119
|
c.openai_organization_id = ruby_llm.openai_organization_id if ruby_llm.openai_organization_id
|
|
111
120
|
c.openai_project_id = ruby_llm.openai_project_id if ruby_llm.openai_project_id
|
|
112
121
|
c.openai_use_system_role = ruby_llm.openai_use_system_role unless ruby_llm.openai_use_system_role.nil?
|
|
113
122
|
end
|
|
114
123
|
|
|
124
|
+
# :reek:UncommunicativeParameterName -- `c` is the RubyLLM config being configured; conventional here.
|
|
115
125
|
def apply_default_models(c)
|
|
116
126
|
c.default_model = ruby_llm.default_model if ruby_llm.default_model
|
|
117
127
|
c.default_embedding_model = ruby_llm.default_embedding_model if ruby_llm.default_embedding_model
|
|
@@ -119,6 +129,7 @@ module RobotLab
|
|
|
119
129
|
c.default_moderation_model = ruby_llm.default_moderation_model if ruby_llm.default_moderation_model
|
|
120
130
|
end
|
|
121
131
|
|
|
132
|
+
# :reek:UncommunicativeParameterName -- `c` is the RubyLLM config being configured; conventional here.
|
|
122
133
|
def apply_connection_settings(c)
|
|
123
134
|
c.request_timeout = ruby_llm.request_timeout if ruby_llm.request_timeout
|
|
124
135
|
c.max_retries = ruby_llm.max_retries if ruby_llm.max_retries
|
|
@@ -128,6 +139,7 @@ module RobotLab
|
|
|
128
139
|
c.http_proxy = ruby_llm.http_proxy if ruby_llm.http_proxy
|
|
129
140
|
end
|
|
130
141
|
|
|
142
|
+
# :reek:UncommunicativeParameterName -- `c` is the RubyLLM config being configured; conventional here.
|
|
131
143
|
def apply_logging_options(c)
|
|
132
144
|
c.log_file = ruby_llm.log_file if ruby_llm.log_file
|
|
133
145
|
c.log_level = ruby_llm.log_level if ruby_llm.log_level
|
|
@@ -145,6 +157,7 @@ module RobotLab
|
|
|
145
157
|
|
|
146
158
|
# Set a RubyLLM config attribute from config value or standard env var.
|
|
147
159
|
# Only sets when a non-nil value is found, to avoid overwriting defaults.
|
|
160
|
+
# :reek:UncommunicativeParameterName -- `c` is the RubyLLM config being configured; conventional here.
|
|
148
161
|
def set_if_present(c, setter, config_key, env_var)
|
|
149
162
|
value = ruby_llm.public_send(config_key) || ENV.fetch(env_var, nil)
|
|
150
163
|
c.public_send(:"#{setter}=", value) if value
|
|
@@ -33,19 +33,21 @@ module RobotLab
|
|
|
33
33
|
@sequence << tool_name.to_s
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
# :reek:TooManyStatements -- self-contained cycle-detection algorithm; splitting the scan loses the shape of the check.
|
|
36
37
|
def doom_loop?
|
|
37
38
|
seq = @sequence
|
|
38
|
-
|
|
39
|
+
length = seq.length
|
|
40
|
+
return false if length < @threshold
|
|
39
41
|
|
|
40
42
|
# Consecutive identical calls: A, A, A
|
|
41
43
|
tail = seq.last(@threshold)
|
|
42
44
|
return true if tail.uniq.length == 1
|
|
43
45
|
|
|
44
46
|
# Cyclic multi-step patterns: A,B,C, A,B,C, A,B,C
|
|
45
|
-
max_period = [MAX_PERIOD,
|
|
47
|
+
max_period = [MAX_PERIOD, length / @threshold].min
|
|
46
48
|
(2..max_period).each do |period|
|
|
47
49
|
window = @threshold * period
|
|
48
|
-
next if
|
|
50
|
+
next if length < window
|
|
49
51
|
|
|
50
52
|
chunk = seq.last(window)
|
|
51
53
|
pattern = chunk.first(period)
|
|
@@ -79,6 +81,7 @@ module RobotLab
|
|
|
79
81
|
|
|
80
82
|
private
|
|
81
83
|
|
|
84
|
+
# :reek:TooManyStatements -- mirrors doom_loop?'s scan to report which period matched.
|
|
82
85
|
def detect_period(seq)
|
|
83
86
|
return 1 if seq.last(@threshold).uniq.length == 1
|
|
84
87
|
|
|
@@ -59,6 +59,7 @@ module RobotLab
|
|
|
59
59
|
# Execute compression and return the new message array.
|
|
60
60
|
#
|
|
61
61
|
# @return [Array] compressed message array
|
|
62
|
+
# :reek:TooManyStatements -- linear classify/score/rebuild pipeline; each early return is a documented no-op case.
|
|
62
63
|
def call
|
|
63
64
|
return @messages if @messages.empty?
|
|
64
65
|
|
|
@@ -113,6 +114,7 @@ module RobotLab
|
|
|
113
114
|
# Determine the action for one compressible message.
|
|
114
115
|
#
|
|
115
116
|
# @return [Symbol, String] :keep, :drop, or a summary String
|
|
117
|
+
# :reek:TooManyStatements -- one linear score-then-threshold decision.
|
|
116
118
|
def score_action(reference, msg)
|
|
117
119
|
text = extract_text(msg)
|
|
118
120
|
|
|
@@ -133,6 +135,7 @@ module RobotLab
|
|
|
133
135
|
end
|
|
134
136
|
|
|
135
137
|
# Build the final message array from the decided actions.
|
|
138
|
+
# :reek:FeatureEnvy -- dispatching on the per-message action value (:keep/:drop/summary) is this method's purpose.
|
|
136
139
|
def build_result(actions)
|
|
137
140
|
result = []
|
|
138
141
|
|
|
@@ -157,6 +160,7 @@ module RobotLab
|
|
|
157
160
|
# System messages and tool-related messages are always pinned.
|
|
158
161
|
# Assistant messages with no text content (tool call dispatchers)
|
|
159
162
|
# are also pinned to avoid breaking tool_use/tool_result pairing.
|
|
163
|
+
# :reek:FeatureEnvy -- classifying by a message's own role and text; Message stays a plain value object.
|
|
160
164
|
def pinned_message?(msg)
|
|
161
165
|
role = msg.role
|
|
162
166
|
|
|
@@ -187,6 +191,7 @@ module RobotLab
|
|
|
187
191
|
#
|
|
188
192
|
# @param vectors [Array<Hash{Symbol => Float}>]
|
|
189
193
|
# @return [Hash{Symbol => Float}]
|
|
194
|
+
# :reek:NestedIterators -- 2-deep each over sparse vectors is the natural element-wise sum.
|
|
190
195
|
def mean_vector(vectors)
|
|
191
196
|
return {} if vectors.empty?
|
|
192
197
|
|
data/lib/robot_lab/hook.rb
CHANGED
|
@@ -4,9 +4,11 @@ module RobotLab
|
|
|
4
4
|
class HookContext
|
|
5
5
|
attr_reader :event, :metadata
|
|
6
6
|
|
|
7
|
+
# :reek:ControlParameter -- `metadata || ExtensionState.new` is a nil-safe default, not behavior selection.
|
|
7
8
|
def initialize(event:, metadata: nil)
|
|
8
9
|
@event = event.to_sym
|
|
9
10
|
@metadata = metadata || ExtensionState.new
|
|
11
|
+
@namespace = nil # no hook namespace active until with_namespace
|
|
10
12
|
end
|
|
11
13
|
|
|
12
14
|
def ext(name)
|
|
@@ -46,6 +48,7 @@ module RobotLab
|
|
|
46
48
|
attr_reader :robot, :network, :task, :memory, :config
|
|
47
49
|
attr_accessor :request, :response, :error
|
|
48
50
|
|
|
51
|
+
# :reek:LongParameterList -- one keyword per run-context field; hooks read them all.
|
|
49
52
|
def initialize(robot:, request:, network: nil, task: nil, memory: nil, config: nil, response: nil, error: nil, **)
|
|
50
53
|
super(event: :run, **)
|
|
51
54
|
@robot = robot
|
|
@@ -104,6 +107,7 @@ module RobotLab
|
|
|
104
107
|
attr_reader :network, :task, :task_name, :robot, :memory, :config
|
|
105
108
|
attr_accessor :result, :error
|
|
106
109
|
|
|
110
|
+
# :reek:ControlParameter -- `robot || task.robot` is a fallback default, not behavior selection.
|
|
107
111
|
def initialize(task:, network: nil, robot: nil, memory: nil, config: nil, result: nil, error: nil, **)
|
|
108
112
|
super(event: :task, **)
|
|
109
113
|
@network = network
|
|
@@ -21,6 +21,7 @@ module RobotLab
|
|
|
21
21
|
# @param handler_class [Class] a RobotLab::Hook subclass
|
|
22
22
|
# @param context [Hash, nil] optional default values merged into ctx.local on each call
|
|
23
23
|
# @return [Registration]
|
|
24
|
+
# :reek:FeatureEnvy -- validating the handler_class argument before registering it is this method's job.
|
|
24
25
|
def on(handler_class, context: nil)
|
|
25
26
|
unless handler_class.is_a?(Class) && handler_class < RobotLab::Hook
|
|
26
27
|
raise ArgumentError, "#{handler_class.inspect} must be a RobotLab::Hook subclass"
|
data/lib/robot_lab/hooks.rb
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RobotLab
|
|
4
|
+
# :reek:DataClump -- stateless module_function dispatchers; registries/per_run_hooks flow through every call by design.
|
|
4
5
|
module Hooks
|
|
5
6
|
module_function
|
|
6
7
|
|
|
8
|
+
# :reek:TooManyStatements -- the before/around/after/error hook lifecycle in one linear sequence.
|
|
7
9
|
def run(family, context, registries:, per_run_hooks: nil, &)
|
|
8
10
|
before = registrations(:"before_#{family}", registries, per_run_hooks)
|
|
9
11
|
around = registrations(:"around_#{family}", registries, per_run_hooks)
|
|
@@ -53,9 +55,10 @@ module RobotLab
|
|
|
53
55
|
end
|
|
54
56
|
|
|
55
57
|
def call_registration(registration, hook_name, context, &)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
namespace = registration.namespace
|
|
59
|
+
context.with_namespace(namespace) do
|
|
60
|
+
if registration.context && namespace
|
|
61
|
+
context.ext(namespace).merge_defaults(registration.context)
|
|
59
62
|
end
|
|
60
63
|
registration.handler_class.call(hook_name, context, &)
|
|
61
64
|
end
|
data/lib/robot_lab/mcp/client.rb
CHANGED
|
@@ -13,6 +13,7 @@ module RobotLab
|
|
|
13
13
|
# tools = client.list_tools
|
|
14
14
|
# result = client.call_tool("createBranch", { project_id: "abc" })
|
|
15
15
|
#
|
|
16
|
+
# :reek:RepeatedConditional -- @connected is the connection-lifecycle guard; every public operation must check it.
|
|
16
17
|
class Client
|
|
17
18
|
# @!attribute [r] server
|
|
18
19
|
# @return [Server] the MCP server configuration
|
|
@@ -45,6 +46,7 @@ module RobotLab
|
|
|
45
46
|
#
|
|
46
47
|
# @return [self]
|
|
47
48
|
#
|
|
49
|
+
# :reek:TooManyStatements -- linear connect/register sequence with a best-effort rescue.
|
|
48
50
|
def connect
|
|
49
51
|
return self if @connected
|
|
50
52
|
|
|
@@ -204,8 +206,6 @@ module RobotLab
|
|
|
204
206
|
end
|
|
205
207
|
|
|
206
208
|
def parse_response(response)
|
|
207
|
-
return response[:result] if response.is_a?(Hash) && response[:result]
|
|
208
|
-
|
|
209
209
|
case response
|
|
210
210
|
when String
|
|
211
211
|
parsed = JSON.parse(response, symbolize_names: true)
|
|
@@ -104,15 +104,20 @@ module RobotLab
|
|
|
104
104
|
# @param timeout [Numeric] seconds before raising MCPError
|
|
105
105
|
# @return [Hash] parsed response
|
|
106
106
|
# @raise [MCPError] on timeout or connection error
|
|
107
|
+
# :reek:TooManyStatements -- register/write/wait/cleanup must stay in one method so ensure releases the queue.
|
|
108
|
+
# :reek:DuplicateMethodCall -- the repeated @mutex.synchronize/@clients[io] pairs are deliberately separate short critical
|
|
109
|
+
# sections; holding the lock across the blocking write/wait would deadlock the poll loop.
|
|
107
110
|
def send_request(client, message, timeout:)
|
|
108
|
-
|
|
109
|
-
|
|
111
|
+
transport = client.transport
|
|
112
|
+
io = transport.stdout
|
|
113
|
+
stdin = transport.stdin
|
|
114
|
+
queue = Thread::Queue.new
|
|
110
115
|
|
|
111
116
|
@mutex.synchronize { @clients[io][:queue] = queue }
|
|
112
117
|
|
|
113
118
|
begin
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
stdin.puts(message.to_json)
|
|
120
|
+
stdin.flush
|
|
116
121
|
rescue Errno::EPIPE, IOError => e
|
|
117
122
|
@mutex.synchronize { @clients[io][:queue] = nil }
|
|
118
123
|
raise MCPError.new("MCP connection lost: #{e.message}", retryable: true)
|
|
@@ -160,6 +165,7 @@ module RobotLab
|
|
|
160
165
|
end
|
|
161
166
|
end
|
|
162
167
|
|
|
168
|
+
# :reek:TooManyStatements -- read/parse/filter/route steps for each readable IO; each guard skips a non-response line.
|
|
163
169
|
def dispatch(readable_ios)
|
|
164
170
|
readable_ios.each do |io|
|
|
165
171
|
line = io.gets rescue nil
|
|
@@ -177,10 +183,12 @@ module RobotLab
|
|
|
177
183
|
end
|
|
178
184
|
|
|
179
185
|
def stdio_client?(client)
|
|
180
|
-
client.respond_to?(:transport)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
186
|
+
return false unless client.respond_to?(:transport)
|
|
187
|
+
|
|
188
|
+
transport = client.transport
|
|
189
|
+
transport.is_a?(Transports::Stdio) &&
|
|
190
|
+
transport.respond_to?(:stdout) &&
|
|
191
|
+
!transport.stdout.nil?
|
|
184
192
|
end
|
|
185
193
|
end
|
|
186
194
|
end
|
|
@@ -60,6 +60,7 @@ module RobotLab
|
|
|
60
60
|
# @param threshold [Float] minimum cosine score (default 0.05)
|
|
61
61
|
# @return [Array<Hash, MCP::Server>] matching servers, or +from+ as
|
|
62
62
|
# fallback when no match is found
|
|
63
|
+
# :reek:TooManyStatements -- linear guard/score/filter pipeline; each early return is a documented fallback.
|
|
63
64
|
def self.select(query, from:, threshold: DEFAULT_THRESHOLD)
|
|
64
65
|
return from if from.empty?
|
|
65
66
|
return from if query.to_s.strip.empty?
|
|
@@ -10,6 +10,8 @@ module RobotLab
|
|
|
10
10
|
# @example
|
|
11
11
|
# transport = SSE.new(url: "http://localhost:8080/sse")
|
|
12
12
|
#
|
|
13
|
+
# :reek:InstanceVariableAssumption -- @config is assigned in Base#initialize (reek does not trace super).
|
|
14
|
+
# :reek:RepeatedConditional -- @connected is the connection-lifecycle guard; every operation must check it.
|
|
13
15
|
class SSE < Base
|
|
14
16
|
# Creates a new SSE transport.
|
|
15
17
|
#
|
|
@@ -26,6 +28,7 @@ module RobotLab
|
|
|
26
28
|
#
|
|
27
29
|
# @return [self]
|
|
28
30
|
# @raise [MCPError] if async-http gem is not available
|
|
31
|
+
# :reek:TooManyStatements -- linear require/connect/handshake sequence inside the Async block.
|
|
29
32
|
def connect
|
|
30
33
|
return self if @connected
|
|
31
34
|
|
|
@@ -19,6 +19,8 @@ module RobotLab
|
|
|
19
19
|
# timeout: 10
|
|
20
20
|
# )
|
|
21
21
|
#
|
|
22
|
+
# :reek:InstanceVariableAssumption -- @config and @timeout are assigned in Base#initialize (reek does not trace super).
|
|
23
|
+
# :reek:RepeatedConditional -- @connected is the connection-lifecycle guard; every IO operation must check it.
|
|
22
24
|
class Stdio < Base
|
|
23
25
|
# Creates a new Stdio transport.
|
|
24
26
|
#
|
|
@@ -41,6 +43,7 @@ module RobotLab
|
|
|
41
43
|
# @return [self]
|
|
42
44
|
# @raise [MCPError] if the server process cannot be started or does not
|
|
43
45
|
# respond to the MCP initialize handshake within the timeout period
|
|
46
|
+
# :reek:TooManyStatements -- linear spawn/verify/handshake sequence with per-failure-mode rescues.
|
|
44
47
|
def connect
|
|
45
48
|
return self if @connected
|
|
46
49
|
|
|
@@ -76,6 +79,7 @@ module RobotLab
|
|
|
76
79
|
# @param message [Hash] JSON-RPC message
|
|
77
80
|
# @return [Hash] the response
|
|
78
81
|
# @raise [MCPError] if not connected, no response, or timeout
|
|
82
|
+
# :reek:TooManyStatements -- write-then-read loop must stay inside the one Timeout block.
|
|
79
83
|
def send_request(message)
|
|
80
84
|
raise MCPError, "Not connected" unless @connected
|
|
81
85
|
|
|
@@ -148,6 +152,7 @@ module RobotLab
|
|
|
148
152
|
@stdin.flush
|
|
149
153
|
end
|
|
150
154
|
|
|
155
|
+
# :reek:TooManyStatements -- best-effort teardown; each handle is closed and nilled independently.
|
|
151
156
|
def cleanup_process
|
|
152
157
|
@connected = false
|
|
153
158
|
@stdin&.close rescue nil
|
|
@@ -13,6 +13,8 @@ module RobotLab
|
|
|
13
13
|
# session_id: "abc123"
|
|
14
14
|
# )
|
|
15
15
|
#
|
|
16
|
+
# :reek:InstanceVariableAssumption -- @config is assigned in Base#initialize (reek does not trace super).
|
|
17
|
+
# :reek:RepeatedConditional -- @connected is the connection-lifecycle guard; every operation must check it.
|
|
16
18
|
class StreamableHTTP < Base
|
|
17
19
|
# Creates a new StreamableHTTP transport.
|
|
18
20
|
#
|
|
@@ -31,6 +33,7 @@ module RobotLab
|
|
|
31
33
|
#
|
|
32
34
|
# @return [self]
|
|
33
35
|
# @raise [MCPError] if async-http gem is not available
|
|
36
|
+
# :reek:TooManyStatements -- linear require/connect/handshake sequence inside the Async block.
|
|
34
37
|
def connect
|
|
35
38
|
return self if @connected
|
|
36
39
|
|
|
@@ -60,6 +63,7 @@ module RobotLab
|
|
|
60
63
|
# @param message [Hash] JSON-RPC message
|
|
61
64
|
# @return [Hash] the response
|
|
62
65
|
# @raise [MCPError] if not connected
|
|
66
|
+
# :reek:TooManyStatements -- header assembly and POST must stay inside the one Async block.
|
|
63
67
|
def send_request(message)
|
|
64
68
|
raise MCPError, "Not connected" unless @connected
|
|
65
69
|
|
|
@@ -10,6 +10,8 @@ module RobotLab
|
|
|
10
10
|
# @example
|
|
11
11
|
# transport = WebSocket.new(url: "ws://localhost:8080")
|
|
12
12
|
#
|
|
13
|
+
# :reek:InstanceVariableAssumption -- @config is assigned in Base#initialize (reek does not trace super).
|
|
14
|
+
# :reek:RepeatedConditional -- @connected is the connection-lifecycle guard; every operation must check it.
|
|
13
15
|
class WebSocket < Base
|
|
14
16
|
# Creates a new WebSocket transport.
|
|
15
17
|
#
|
|
@@ -26,6 +28,7 @@ module RobotLab
|
|
|
26
28
|
#
|
|
27
29
|
# @return [self]
|
|
28
30
|
# @raise [MCPError] if async-websocket gem is not available
|
|
31
|
+
# :reek:TooManyStatements -- linear require/connect/handshake sequence inside the Async block.
|
|
29
32
|
def connect
|
|
30
33
|
return self if @connected
|
|
31
34
|
|
data/lib/robot_lab/memory.rb
CHANGED
|
@@ -58,6 +58,9 @@ module RobotLab
|
|
|
58
58
|
# memory.results # => []
|
|
59
59
|
# memory.cache # => RubyLLM::SemanticCache instance
|
|
60
60
|
#
|
|
61
|
+
# :reek:TooManyMethods -- Memory is the deliberately broad shared-state facade
|
|
62
|
+
# (reserved keys, reactive get/subscribe, history); see CLAUDE.md.
|
|
63
|
+
# :reek:RepeatedConditional -- `@backend.key?(key)` is the check-under-mutex idiom; each site is a separate critical section.
|
|
61
64
|
class Memory
|
|
62
65
|
include Utils
|
|
63
66
|
|
|
@@ -89,6 +92,7 @@ module RobotLab
|
|
|
89
92
|
#
|
|
90
93
|
# @example Network-owned memory
|
|
91
94
|
# Memory.new(network_name: "support_pipeline")
|
|
95
|
+
# :reek:BooleanParameter -- enable_cache is a documented feature toggle in the public API.
|
|
92
96
|
def initialize(data: {}, results: [], messages: [], session_id: nil, backend: :auto, enable_cache: true,
|
|
93
97
|
network_name: nil)
|
|
94
98
|
@backend = select_backend(backend)
|
|
@@ -144,6 +148,7 @@ module RobotLab
|
|
|
144
148
|
#
|
|
145
149
|
# @see #set
|
|
146
150
|
#
|
|
151
|
+
# :reek:TooManyStatements -- one case branch per reserved key; dispatch reads best as a single table.
|
|
147
152
|
def []=(key, value)
|
|
148
153
|
key = key.to_sym
|
|
149
154
|
|
|
@@ -292,6 +297,8 @@ module RobotLab
|
|
|
292
297
|
# memory.get(:sentiment, :entities, :keywords, wait: 60)
|
|
293
298
|
# # => { sentiment: {...}, entities: [...], keywords: [...] }
|
|
294
299
|
#
|
|
300
|
+
# :reek:BooleanParameter -- `wait` is public API: false, true (block forever) or a numeric timeout.
|
|
301
|
+
# :reek:FeatureEnvy -- normalizing this method's own varargs before dispatching on arity.
|
|
295
302
|
def get(*keys, wait: false)
|
|
296
303
|
keys = keys.flatten.map(&:to_sym)
|
|
297
304
|
|
|
@@ -370,6 +377,8 @@ module RobotLab
|
|
|
370
377
|
# @param subscription_id [Object] the subscription identifier from subscribe
|
|
371
378
|
# @return [Boolean] true if subscription was found and removed
|
|
372
379
|
#
|
|
380
|
+
# :reek:ControlParameter -- subscription_id is matched against stored ids, not used to select behavior.
|
|
381
|
+
# :reek:NestedIterators -- 2-deep scan of the per-key subscription lists is the natural shape.
|
|
373
382
|
def unsubscribe(subscription_id)
|
|
374
383
|
removed = false
|
|
375
384
|
|
|
@@ -576,6 +585,7 @@ module RobotLab
|
|
|
576
585
|
#
|
|
577
586
|
# @return [self]
|
|
578
587
|
#
|
|
588
|
+
# :reek:TooManyStatements -- reserved keys are re-seeded one by one inside a single mutex block.
|
|
579
589
|
def reset
|
|
580
590
|
cached = get_internal(:cache) # Preserve cache instance
|
|
581
591
|
@mutex.synchronize do
|
|
@@ -695,6 +705,7 @@ module RobotLab
|
|
|
695
705
|
RubyLLM::SemanticCache
|
|
696
706
|
end
|
|
697
707
|
|
|
708
|
+
# :reek:ControlParameter -- factory method; the preference symbol is exactly what selects the backend.
|
|
698
709
|
def select_backend(preference)
|
|
699
710
|
case preference
|
|
700
711
|
when :hash
|
|
@@ -716,11 +727,11 @@ module RobotLab
|
|
|
716
727
|
{}
|
|
717
728
|
end
|
|
718
729
|
|
|
719
|
-
def redis_available?
|
|
730
|
+
def redis_available?(config = RobotLab.config)
|
|
720
731
|
return false unless defined?(Redis)
|
|
721
732
|
|
|
722
733
|
# Check if Redis is configured in RobotLab
|
|
723
|
-
redis_config =
|
|
734
|
+
redis_config = config.respond_to?(:redis) ? config.redis : nil
|
|
724
735
|
redis_config || ENV.fetch("REDIS_URL", nil)
|
|
725
736
|
end
|
|
726
737
|
|
|
@@ -766,6 +777,7 @@ module RobotLab
|
|
|
766
777
|
wait_for_key(key, timeout: timeout)
|
|
767
778
|
end
|
|
768
779
|
|
|
780
|
+
# :reek:TooManyStatements -- read-under-mutex then wait-for-missing; splitting would separate lock from wait logic.
|
|
769
781
|
def get_multiple(keys, wait:)
|
|
770
782
|
results = {}
|
|
771
783
|
missing = []
|
|
@@ -791,6 +803,7 @@ module RobotLab
|
|
|
791
803
|
results
|
|
792
804
|
end
|
|
793
805
|
|
|
806
|
+
# :reek:TooManyStatements -- double-check locking plus timeout cleanup must stay together for correctness.
|
|
794
807
|
def wait_for_key(key, timeout:)
|
|
795
808
|
waiter = Waiter.new
|
|
796
809
|
|
|
@@ -813,11 +826,13 @@ module RobotLab
|
|
|
813
826
|
result
|
|
814
827
|
end
|
|
815
828
|
|
|
829
|
+
# :reek:UncommunicativeVariableName -- single-char block vars are accepted style here (RuboCop allows them).
|
|
816
830
|
def wake_waiters(key, value)
|
|
817
831
|
waiters = @waiter_mutex.synchronize { @waiters.delete(key) || [] }
|
|
818
832
|
waiters.each { |w| w.signal(value) }
|
|
819
833
|
end
|
|
820
834
|
|
|
835
|
+
# :reek:TooManyStatements -- collect/build/coalesce steps share the change object and the scheduling flag.
|
|
821
836
|
def notify_subscribers_async(key, value, old_value)
|
|
822
837
|
# Collect all matching subscribers
|
|
823
838
|
callbacks = []
|
|
@@ -860,6 +875,8 @@ module RobotLab
|
|
|
860
875
|
# Drain all pending notification batches in a single fiber.
|
|
861
876
|
# Loops until the queue is empty, then resets the drainer flag.
|
|
862
877
|
# If new items arrive just before the flag resets, reschedules itself.
|
|
878
|
+
# :reek:TooManyStatements :reek:NestedIterators -- the drain loop and its ensure-reschedule race guard are one
|
|
879
|
+
# atomic unit; batch-of-callbacks iteration is inherently 2-deep.
|
|
863
880
|
def drain_notification_queue
|
|
864
881
|
loop do
|
|
865
882
|
batch = @notification_queue_mutex.synchronize do
|
|
@@ -908,8 +925,8 @@ module RobotLab
|
|
|
908
925
|
#
|
|
909
926
|
# @api private
|
|
910
927
|
class RedisBackend
|
|
911
|
-
def initialize
|
|
912
|
-
@redis = create_redis_connection
|
|
928
|
+
def initialize(config = RobotLab.config)
|
|
929
|
+
@redis = create_redis_connection(config)
|
|
913
930
|
@namespace = "robot_lab:memory:#{SecureRandom.uuid}"
|
|
914
931
|
end
|
|
915
932
|
|
|
@@ -945,8 +962,8 @@ module RobotLab
|
|
|
945
962
|
|
|
946
963
|
private
|
|
947
964
|
|
|
948
|
-
def create_redis_connection
|
|
949
|
-
redis_config =
|
|
965
|
+
def create_redis_connection(config = RobotLab.config)
|
|
966
|
+
redis_config = config.respond_to?(:redis) ? config.redis : nil
|
|
950
967
|
|
|
951
968
|
if redis_config.is_a?(Hash)
|
|
952
969
|
Redis.new(**redis_config)
|
|
@@ -43,6 +43,7 @@ module RobotLab
|
|
|
43
43
|
# @param timestamp [Time] when the change occurred (defaults to now)
|
|
44
44
|
# @param correlation_id [String, nil] optional correlation ID
|
|
45
45
|
#
|
|
46
|
+
# :reek:ControlParameter -- `timestamp || Time.now` is a nil-safe default, not behavior selection.
|
|
46
47
|
def initialize(key:, value:, previous: nil, writer: nil, network_name: nil, timestamp: nil, correlation_id: nil)
|
|
47
48
|
@key = key.to_sym
|
|
48
49
|
@value = value
|
data/lib/robot_lab/message.rb
CHANGED
|
@@ -178,6 +178,7 @@ module RobotLab
|
|
|
178
178
|
# @param id [String] the unique identifier for this tool call
|
|
179
179
|
# @param name [String] the name of the tool
|
|
180
180
|
# @param input [Hash, nil] the input arguments
|
|
181
|
+
# :reek:ControlParameter -- `input || {}` is a nil-safe default, not behavior selection.
|
|
181
182
|
def initialize(id:, name:, input:)
|
|
182
183
|
@id = id
|
|
183
184
|
@name = name
|
|
@@ -238,6 +239,7 @@ module RobotLab
|
|
|
238
239
|
# @param role [String, Symbol] the message role (usually assistant)
|
|
239
240
|
# @param tools [Array<ToolMessage, Hash>] the tool calls
|
|
240
241
|
# @param stop_reason [String, Symbol, nil] the stop reason (defaults to "tool")
|
|
242
|
+
# :reek:ControlParameter -- `stop_reason || "tool"` is a nil-safe default, not behavior selection.
|
|
241
243
|
def initialize(role:, tools:, stop_reason: nil)
|
|
242
244
|
@tools = normalize_tools(tools)
|
|
243
245
|
super(type: "tool_call", role: role, content: nil, stop_reason: stop_reason || "tool")
|
|
@@ -295,6 +297,7 @@ module RobotLab
|
|
|
295
297
|
# @param tool [ToolMessage, Hash] the tool call that was executed
|
|
296
298
|
# @param content [Hash] the result content (with :data or :error key)
|
|
297
299
|
# @param stop_reason [String, Symbol, nil] the stop reason (defaults to "tool")
|
|
300
|
+
# :reek:ControlParameter -- `stop_reason || "tool"` is a nil-safe default, not behavior selection.
|
|
298
301
|
def initialize(tool:, content:, stop_reason: nil)
|
|
299
302
|
@tool = normalize_tool(tool)
|
|
300
303
|
super(type: "tool_result", role: "tool_result", content: content, stop_reason: stop_reason || "tool")
|