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.
Files changed (153) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +4 -0
  3. data/.loki +5 -0
  4. data/Archspec.rb +44 -0
  5. data/CHANGELOG.md +17 -1
  6. data/README.md +124 -64
  7. data/Rakefile +6 -111
  8. data/_typos.toml +21 -0
  9. data/docs/api/core/index.md +41 -15
  10. data/docs/api/core/memory.md +247 -29
  11. data/docs/api/core/network.md +255 -33
  12. data/docs/api/core/result.md +120 -32
  13. data/docs/api/core/robot.md +551 -61
  14. data/docs/api/core/state.md +87 -197
  15. data/docs/api/core/tool.md +165 -20
  16. data/docs/api/errors.md +110 -17
  17. data/docs/api/hooks.md +469 -0
  18. data/docs/api/index.md +80 -7
  19. data/docs/api/mcp/client.md +129 -35
  20. data/docs/api/mcp/index.md +164 -23
  21. data/docs/api/mcp/server.md +27 -3
  22. data/docs/api/mcp/transports.md +94 -22
  23. data/docs/api/messages/index.md +26 -3
  24. data/docs/api/messages/text-message.md +33 -11
  25. data/docs/api/messages/tool-call-message.md +27 -4
  26. data/docs/api/messages/tool-result-message.md +23 -4
  27. data/docs/api/messages/user-message.md +45 -8
  28. data/docs/api/skills.md +423 -0
  29. data/docs/api/streaming/context.md +28 -5
  30. data/docs/api/streaming/index.md +57 -11
  31. data/docs/api/support.md +846 -0
  32. data/docs/architecture/core-concepts.md +79 -31
  33. data/docs/architecture/index.md +87 -11
  34. data/docs/architecture/message-flow.md +66 -29
  35. data/docs/architecture/network-orchestration.md +145 -38
  36. data/docs/architecture/robot-execution.md +172 -90
  37. data/docs/architecture/state-management.md +31 -12
  38. data/docs/concepts.md +176 -21
  39. data/docs/examples/basic-chat.md +72 -19
  40. data/docs/examples/index.md +117 -31
  41. data/docs/examples/mcp-server.md +154 -45
  42. data/docs/examples/multi-robot-network.md +91 -21
  43. data/docs/examples/tool-usage.md +104 -37
  44. data/docs/getting-started/configuration.md +288 -98
  45. data/docs/getting-started/installation.md +53 -41
  46. data/docs/getting-started/quick-start.md +51 -6
  47. data/docs/guides/building-robots.md +258 -50
  48. data/docs/guides/creating-networks.md +214 -30
  49. data/docs/guides/hooks.md +263 -54
  50. data/docs/guides/knowledge.md +35 -4
  51. data/docs/guides/mcp-integration.md +211 -44
  52. data/docs/guides/memory.md +103 -12
  53. data/docs/guides/observability.md +95 -47
  54. data/docs/guides/streaming.md +184 -125
  55. data/docs/guides/using-tools.md +247 -18
  56. data/docs/index.md +36 -4
  57. data/examples/01_simple_robot.rb +2 -2
  58. data/examples/02_tools.rb +14 -4
  59. data/examples/03_network.rb +12 -7
  60. data/examples/04_mcp.rb +11 -4
  61. data/examples/05_streaming.rb +8 -5
  62. data/examples/06_prompt_templates.rb +13 -9
  63. data/examples/07_network_memory.rb +5 -5
  64. data/examples/08_llm_config.rb +20 -15
  65. data/examples/09_chaining.rb +4 -4
  66. data/examples/11_network_introspection.rb +4 -4
  67. data/examples/12_message_bus.rb +2 -2
  68. data/examples/13_spawn.rb +2 -2
  69. data/examples/14_rusty_circuit/README.md +1 -0
  70. data/examples/14_rusty_circuit/comic.rb +7 -3
  71. data/examples/14_rusty_circuit/display.rb +14 -0
  72. data/examples/14_rusty_circuit/heckler.rb +8 -6
  73. data/examples/14_rusty_circuit/open_mic.rb +17 -6
  74. data/examples/14_rusty_circuit/scout.rb +17 -10
  75. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +14 -10
  76. data/examples/15_memory_network_and_bus/linux_writer.rb +2 -2
  77. data/examples/15_memory_network_and_bus/os_editor.rb +3 -1
  78. data/examples/15_memory_network_and_bus/os_writer.rb +4 -1
  79. data/examples/16_writers_room/writer.rb +22 -22
  80. data/examples/16_writers_room/writers_room.rb +2 -0
  81. data/examples/17_skills.rb +14 -13
  82. data/examples/18_rails/README.md +20 -1
  83. data/examples/18_rails/app/controllers/chat_controller.rb +5 -1
  84. data/examples/18_rails/app/jobs/robot_run_job.rb +11 -5
  85. data/examples/18_rails/app/robots/chat_robot.rb +11 -0
  86. data/examples/18_rails/config/initializers/robot_lab.rb +8 -0
  87. data/examples/19_token_tracking.rb +25 -9
  88. data/examples/20_circuit_breaker.rb +10 -7
  89. data/examples/21_learning_loop.rb +42 -16
  90. data/examples/22_context_compression.rb +23 -23
  91. data/examples/23_convergence.rb +24 -17
  92. data/examples/24_structured_delegation.rb +13 -8
  93. data/examples/25_history_search.rb +12 -8
  94. data/examples/27_incident_response/incident_response.rb +31 -13
  95. data/examples/28_mcp_discovery.rb +17 -13
  96. data/examples/29_ractor_tools.rb +4 -2
  97. data/examples/30_ractor_network.rb +22 -17
  98. data/examples/31_launch_assessment.rb +20 -9
  99. data/examples/32_newsletter_reader.rb +7 -2
  100. data/examples/33_stock_predictor.rb +34 -13
  101. data/examples/34_agentskills.rb +7 -3
  102. data/examples/35_hooks.rb +18 -8
  103. data/examples/README.md +199 -45
  104. data/examples/common.rb +79 -11
  105. data/examples/xyzzy.rb +8 -1
  106. data/lib/robot_lab/agent_skill_catalog.rb +1 -0
  107. data/lib/robot_lab/ask_user.rb +2 -0
  108. data/lib/robot_lab/bus_poller.rb +2 -0
  109. data/lib/robot_lab/capabilities.rb +4 -0
  110. data/lib/robot_lab/config.rb +18 -5
  111. data/lib/robot_lab/doom_loop_detector.rb +6 -3
  112. data/lib/robot_lab/history_compressor.rb +5 -0
  113. data/lib/robot_lab/hook.rb +1 -0
  114. data/lib/robot_lab/hook_context.rb +4 -0
  115. data/lib/robot_lab/hook_registry.rb +1 -0
  116. data/lib/robot_lab/hooks.rb +6 -3
  117. data/lib/robot_lab/mcp/client.rb +2 -2
  118. data/lib/robot_lab/mcp/connection_poller.rb +16 -8
  119. data/lib/robot_lab/mcp/server_discovery.rb +1 -0
  120. data/lib/robot_lab/mcp/transports/sse.rb +3 -0
  121. data/lib/robot_lab/mcp/transports/stdio.rb +5 -0
  122. data/lib/robot_lab/mcp/transports/streamable_http.rb +4 -0
  123. data/lib/robot_lab/mcp/transports/websocket.rb +3 -0
  124. data/lib/robot_lab/memory.rb +23 -6
  125. data/lib/robot_lab/memory_change.rb +1 -0
  126. data/lib/robot_lab/message.rb +3 -0
  127. data/lib/robot_lab/names.rb +400 -0
  128. data/lib/robot_lab/network.rb +13 -6
  129. data/lib/robot_lab/robot/agent_skill_matching.rb +3 -3
  130. data/lib/robot_lab/robot/bus_messaging.rb +19 -8
  131. data/lib/robot_lab/robot/history_search.rb +2 -0
  132. data/lib/robot_lab/robot/hooking.rb +3 -0
  133. data/lib/robot_lab/robot/mcp_management.rb +11 -3
  134. data/lib/robot_lab/robot/template_rendering.rb +29 -7
  135. data/lib/robot_lab/robot.rb +81 -19
  136. data/lib/robot_lab/robot_result.rb +3 -0
  137. data/lib/robot_lab/run_config.rb +5 -0
  138. data/lib/robot_lab/script_tool.rb +20 -39
  139. data/lib/robot_lab/state_proxy.rb +1 -0
  140. data/lib/robot_lab/streaming/context.rb +1 -0
  141. data/lib/robot_lab/streaming/events.rb +1 -0
  142. data/lib/robot_lab/task.rb +2 -0
  143. data/lib/robot_lab/tool.rb +4 -0
  144. data/lib/robot_lab/user_message.rb +1 -0
  145. data/lib/robot_lab/utils.rb +2 -0
  146. data/lib/robot_lab/version.rb +1 -1
  147. data/lib/robot_lab/waiter.rb +3 -0
  148. data/lib/robot_lab.rb +27 -18
  149. data/mkdocs.yml +6 -1
  150. metadata +12 -7
  151. data/lib/robot_lab/sandbox/null.rb +0 -13
  152. data/lib/robot_lab/sandbox/seatbelt.rb +0 -104
  153. data/lib/robot_lab/sandbox.rb +0 -52
@@ -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/config.yml)
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/config.yml)
25
- # defaults:
26
- # ruby_llm:
27
- # anthropic_api_key: <%= ENV['ANTHROPIC_API_KEY'] %>
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
- return false if seq.length < @threshold
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, seq.length / @threshold].min
47
+ max_period = [MAX_PERIOD, length / @threshold].min
46
48
  (2..max_period).each do |period|
47
49
  window = @threshold * period
48
- next if seq.length < window
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
 
@@ -31,6 +31,7 @@ module RobotLab
31
31
  # robot.on(AuditHook)
32
32
  # network.on(AuditHook)
33
33
  #
34
+ # :reek:InstanceVariableAssumption -- @namespace is a class-level ivar, initialized here and reset in .inherited; there is no #initialize.
34
35
  class Hook
35
36
  @namespace = nil
36
37
 
@@ -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"
@@ -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
- context.with_namespace(registration.namespace) do
57
- if registration.context && registration.namespace
58
- context.ext(registration.namespace).merge_defaults(registration.context)
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
@@ -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
- io = client.transport.stdout
109
- queue = Thread::Queue.new
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
- client.transport.stdin.puts(message.to_json)
115
- client.transport.stdin.flush
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
- client.transport.is_a?(Transports::Stdio) &&
182
- client.transport.respond_to?(:stdout) &&
183
- !client.transport.stdout.nil?
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
 
@@ -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 = RobotLab.config.respond_to?(:redis) ? RobotLab.config.redis : nil
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 = RobotLab.config.respond_to?(:redis) ? RobotLab.config.redis : nil
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
@@ -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")