robot_lab 0.2.7 → 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/Rakefile +6 -111
- data/_typos.toml +21 -0
- data/docs/api/index.md +2 -2
- data/docs/api/skills.md +53 -149
- data/docs/api/support.md +1 -1
- data/docs/architecture/index.md +5 -4
- data/docs/getting-started/configuration.md +4 -1
- data/docs/guides/hooks.md +122 -0
- data/docs/guides/using-tools.md +10 -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 +8 -0
- 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 +2 -4
- data/lib/robot_lab/network.rb +13 -6
- data/lib/robot_lab/robot/agent_skill_matching.rb +2 -0
- data/lib/robot_lab/robot/bus_messaging.rb +3 -0
- 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 +13 -4
- data/lib/robot_lab/robot.rb +58 -17
- 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 +6 -3
- metadata +7 -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/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")
|
data/lib/robot_lab/names.rb
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
# A large curated word list, not application logic — kept as one inline
|
|
4
4
|
# module for lookup speed, so the size/length cops below are false positives.
|
|
5
|
-
# rubocop:disable Metrics/ModuleLength
|
|
5
|
+
# rubocop:disable-next Metrics/ModuleLength
|
|
6
6
|
module RobotLab
|
|
7
7
|
# Fictional robot names
|
|
8
|
-
# rubocop:disable Metrics/CollectionLiteralLength
|
|
8
|
+
# rubocop:disable-next Metrics/CollectionLiteralLength
|
|
9
9
|
NAMES = %w[
|
|
10
10
|
R_7723
|
|
11
11
|
R_790
|
|
@@ -394,9 +394,7 @@ module RobotLab
|
|
|
394
394
|
Zhora
|
|
395
395
|
Zoromes
|
|
396
396
|
].freeze
|
|
397
|
-
# rubocop:enable Metrics/CollectionLiteralLength
|
|
398
397
|
|
|
399
398
|
def self.name = names.first
|
|
400
399
|
def self.names(how_many = 1) = NAMES.sample(how_many).sort
|
|
401
400
|
end
|
|
402
|
-
# rubocop:enable Metrics/ModuleLength
|
data/lib/robot_lab/network.rb
CHANGED
|
@@ -87,6 +87,7 @@ module RobotLab
|
|
|
87
87
|
# task :billing, billing_robot, context: { dept: "billing" }, depends_on: :optional
|
|
88
88
|
# end
|
|
89
89
|
#
|
|
90
|
+
# :reek:ControlParameter -- `memory || ...` and `config || ...` are nil-safe defaults, not behavior selection.
|
|
90
91
|
def initialize(name:, concurrency: :auto, memory: nil, config: nil, parallel_mode: :async, &)
|
|
91
92
|
@name = name.to_s
|
|
92
93
|
@robots = {}
|
|
@@ -125,6 +126,7 @@ module RobotLab
|
|
|
125
126
|
# @example Task with dependencies
|
|
126
127
|
# task :writer, writer_robot, depends_on: [:analyst]
|
|
127
128
|
#
|
|
129
|
+
# :reek:LongParameterList -- one keyword per documented per-task option.
|
|
128
130
|
def task(name, robot, context: {}, mcp: :none, tools: :none, memory: nil, config: nil, depends_on: :none,
|
|
129
131
|
poller_group: :default)
|
|
130
132
|
task_wrapper = Task.new(
|
|
@@ -180,6 +182,7 @@ module RobotLab
|
|
|
180
182
|
# result.value # => RobotResult from last robot
|
|
181
183
|
# result.context[:classifier] # => RobotResult from classifier
|
|
182
184
|
#
|
|
185
|
+
# :reek:TooManyStatements -- linear assembly of run params and hook context before dispatch.
|
|
183
186
|
def run(message = nil, **run_context)
|
|
184
187
|
# Runnable protocol: accept a positional message like Robot#run does, so
|
|
185
188
|
# callers can `run(msg, ...)` uniformly. `run(message: msg)` still works.
|
|
@@ -200,12 +203,13 @@ module RobotLab
|
|
|
200
203
|
)
|
|
201
204
|
|
|
202
205
|
RobotLab::Hooks.run(:network_run, context, registries: [RobotLab.hooks, @hooks]) do
|
|
206
|
+
run_params = context.context
|
|
203
207
|
if @parallel_mode == :ractor
|
|
204
|
-
run_with_ractor_scheduler(
|
|
208
|
+
run_with_ractor_scheduler(run_params)
|
|
205
209
|
else
|
|
206
210
|
initial_result = SimpleFlow::Result.new(
|
|
207
|
-
|
|
208
|
-
context: { run_params:
|
|
211
|
+
run_params,
|
|
212
|
+
context: { run_params: run_params }
|
|
209
213
|
)
|
|
210
214
|
@pipeline.call_parallel(initial_result, max_concurrent: @config.max_concurrent_robots)
|
|
211
215
|
end
|
|
@@ -329,11 +333,12 @@ module RobotLab
|
|
|
329
333
|
# @raise [ArgumentError] if a robot with the same name already exists
|
|
330
334
|
#
|
|
331
335
|
def add_robot(robot)
|
|
332
|
-
|
|
333
|
-
|
|
336
|
+
name = robot.name
|
|
337
|
+
if @robots.key?(name)
|
|
338
|
+
raise ArgumentError, "Robot '#{name}' already exists in network '#{@name}'"
|
|
334
339
|
end
|
|
335
340
|
|
|
336
|
-
@robots[
|
|
341
|
+
@robots[name] = robot
|
|
337
342
|
self
|
|
338
343
|
end
|
|
339
344
|
|
|
@@ -398,6 +403,7 @@ module RobotLab
|
|
|
398
403
|
|
|
399
404
|
private
|
|
400
405
|
|
|
406
|
+
# :reek:TooManyStatements -- linear build-specs/run/shutdown sequence for the ractor scheduler.
|
|
401
407
|
def run_with_ractor_scheduler(run_context)
|
|
402
408
|
unless RobotLab.extension_loaded?(:ractor)
|
|
403
409
|
raise RobotLab::DependencyError,
|
|
@@ -419,6 +425,7 @@ module RobotLab
|
|
|
419
425
|
results
|
|
420
426
|
end
|
|
421
427
|
|
|
428
|
+
# :reek:FeatureEnvy -- snapshotting a robot's identity fields into a Ractor-shareable spec is this method's job.
|
|
422
429
|
def build_robot_spec(task_wrapper)
|
|
423
430
|
robot = task_wrapper.robot
|
|
424
431
|
RobotSpec.new(
|
|
@@ -40,6 +40,8 @@ module RobotLab
|
|
|
40
40
|
# @param message [String]
|
|
41
41
|
# @param threshold [Float] cosine similarity cutoff (default SIMILARITY_THRESHOLD)
|
|
42
42
|
# @return [Array<AgentSkill>]
|
|
43
|
+
# :reek:TooManyStatements -- linear search/filter with a best-effort rescue fallback.
|
|
44
|
+
# :reek:NestedIterators -- 2-deep filter_map/find joining search hits back to pending skills.
|
|
43
45
|
def match_agent_skills(message, threshold: SIMILARITY_THRESHOLD)
|
|
44
46
|
return [] if @pending_agent_skills.nil? || @pending_agent_skills.empty?
|
|
45
47
|
|
|
@@ -91,6 +91,7 @@ module RobotLab
|
|
|
91
91
|
# @param auto_reply [Boolean] send the responder's result back to the sender
|
|
92
92
|
# @yield [message] the inbound task; return the reply content (nil => no reply)
|
|
93
93
|
# @return [self]
|
|
94
|
+
# :reek:BooleanParameter :reek:ControlParameter -- auto_reply is a documented public API toggle for reply behavior.
|
|
94
95
|
def respond_to_tasks(auto_reply: true, &responder)
|
|
95
96
|
on_message do |message|
|
|
96
97
|
next if message.reply?
|
|
@@ -107,6 +108,7 @@ module RobotLab
|
|
|
107
108
|
#
|
|
108
109
|
# @param auto_reply [Boolean]
|
|
109
110
|
# @return [self]
|
|
111
|
+
# :reek:BooleanParameter -- auto_reply is a documented public API toggle, forwarded to respond_to_tasks.
|
|
110
112
|
def serve(auto_reply: true)
|
|
111
113
|
respond_to_tasks(auto_reply: auto_reply) { |message| run(bus_task_content(message)).reply }
|
|
112
114
|
end
|
|
@@ -161,6 +163,7 @@ module RobotLab
|
|
|
161
163
|
# @param bus [TypedBus::MessageBus, nil] bus to join (creates one if nil)
|
|
162
164
|
# @return [self]
|
|
163
165
|
#
|
|
166
|
+
# :reek:ControlParameter -- `bus || @bus || new` implements the documented join-or-create semantics.
|
|
164
167
|
def with_bus(bus = nil)
|
|
165
168
|
return self if bus && @bus == bus
|
|
166
169
|
|
|
@@ -32,6 +32,8 @@ module RobotLab
|
|
|
32
32
|
# @param limit [Integer] maximum number of results to return (default 5)
|
|
33
33
|
# @return [Array<HistoryResult>] results sorted by score descending
|
|
34
34
|
# @raise [RobotLab::DependencyError] if the 'classifier' gem is not installed
|
|
35
|
+
# :reek:TooManyStatements -- linear vectorize/score/collect scan over the chat history.
|
|
36
|
+
# :reek:FeatureEnvy -- scoring each message's extracted text against the query is the search itself.
|
|
35
37
|
def search_history(query, limit: 5)
|
|
36
38
|
TextAnalysis.require_classifier!
|
|
37
39
|
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
module RobotLab
|
|
4
4
|
class Robot < RubyLLM::Agent
|
|
5
5
|
module Hooking
|
|
6
|
+
# :reek:LongParameterList -- the documented Robot#run public API: each keyword is a distinct run-scoped override.
|
|
7
|
+
# :reek:TooManyStatements -- the run lifecycle (memory writer swap, hook wrap, budget, cleanup) is one
|
|
8
|
+
# deliberate orchestrator; flog gates its complexity.
|
|
6
9
|
def run(message = nil, network: nil, task: nil, network_memory: nil, network_config: nil,
|
|
7
10
|
memory: nil, mcp: :none, tools: :none, hooks: nil, **kwargs, &block)
|
|
8
11
|
run_memory = resolve_run_memory(memory, network: network, network_memory: network_memory)
|
|
@@ -11,6 +11,7 @@ module RobotLab
|
|
|
11
11
|
private
|
|
12
12
|
|
|
13
13
|
# Resolve MCP hierarchy: runtime -> robot build -> network -> config
|
|
14
|
+
# :reek:ControlParameter -- `network_config&.mcp || ...` is the documented fallback cascade, not a mode switch.
|
|
14
15
|
def resolve_mcp_hierarchy(runtime_value, network: nil, network_config: nil)
|
|
15
16
|
parent_value = network_config&.mcp || network_parent_config(network)&.mcp || RobotLab.config.mcp
|
|
16
17
|
build_resolved = ToolConfig.resolve_mcp(@mcp_config, parent_value: parent_value)
|
|
@@ -18,6 +19,7 @@ module RobotLab
|
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
# Resolve tools hierarchy: runtime -> robot build -> network -> config
|
|
22
|
+
# :reek:ControlParameter -- `network_config&.tools || ...` is the documented fallback cascade, not a mode switch.
|
|
21
23
|
def resolve_tools_hierarchy(runtime_value, network: nil, network_config: nil)
|
|
22
24
|
parent_value = network_config&.tools || network_parent_config(network)&.tools || RobotLab.config.tools
|
|
23
25
|
build_resolved = ToolConfig.resolve_tools(@tools_config, parent_value: parent_value)
|
|
@@ -32,6 +34,7 @@ module RobotLab
|
|
|
32
34
|
|
|
33
35
|
# Ensure MCP clients are initialized for the given server configs.
|
|
34
36
|
# On subsequent calls, retries any servers that previously failed to connect.
|
|
37
|
+
# :reek:TooManyStatements -- first-run init vs retry paths share the needed-server list; linear either way.
|
|
35
38
|
def ensure_mcp_clients(mcp_servers)
|
|
36
39
|
return if mcp_servers.empty?
|
|
37
40
|
|
|
@@ -54,6 +57,7 @@ module RobotLab
|
|
|
54
57
|
@mcp_initialized = true
|
|
55
58
|
end
|
|
56
59
|
|
|
60
|
+
# :reek:TooManyStatements -- connect success/failure bookkeeping plus the rescue path for one server.
|
|
57
61
|
def init_mcp_client(server_config)
|
|
58
62
|
client = MCP::Client.new(server_config)
|
|
59
63
|
client.connect
|
|
@@ -78,6 +82,7 @@ module RobotLab
|
|
|
78
82
|
end
|
|
79
83
|
|
|
80
84
|
# Retry connecting to servers that previously failed
|
|
85
|
+
# :reek:TooManyStatements -- per-server retry with success bookkeeping and a best-effort rescue.
|
|
81
86
|
def retry_failed_servers(_mcp_servers, needed_servers)
|
|
82
87
|
return if @failed_mcp_configs.nil? || @failed_mcp_configs.empty?
|
|
83
88
|
|
|
@@ -85,8 +90,9 @@ module RobotLab
|
|
|
85
90
|
to_retry = @failed_mcp_configs.slice(*needed_servers)
|
|
86
91
|
return if to_retry.empty?
|
|
87
92
|
|
|
93
|
+
logger = RobotLab.config.logger
|
|
88
94
|
to_retry.each do |name, server_config|
|
|
89
|
-
|
|
95
|
+
logger.info(
|
|
90
96
|
"Robot '#{@name}' retrying MCP server: #{name}"
|
|
91
97
|
)
|
|
92
98
|
|
|
@@ -97,17 +103,19 @@ module RobotLab
|
|
|
97
103
|
@mcp_clients[name] = client
|
|
98
104
|
@failed_mcp_configs.delete(name)
|
|
99
105
|
discover_mcp_tools(client, name)
|
|
100
|
-
|
|
106
|
+
logger.info(
|
|
101
107
|
"Robot '#{@name}' successfully connected to MCP server '#{name}' on retry"
|
|
102
108
|
)
|
|
103
109
|
end
|
|
104
110
|
rescue StandardError => e
|
|
105
|
-
|
|
111
|
+
logger.warn(
|
|
106
112
|
"Robot '#{@name}' retry failed for MCP server '#{name}': #{e.message}"
|
|
107
113
|
)
|
|
108
114
|
end
|
|
109
115
|
end
|
|
110
116
|
|
|
117
|
+
# :reek:FeatureEnvy -- adapting each MCP tool definition into a local Tool is exactly this method's purpose.
|
|
118
|
+
# :reek:NestedIterators -- the inner block is the tool's execution closure, not an iteration.
|
|
111
119
|
def discover_mcp_tools(client, server_name)
|
|
112
120
|
tools = client.list_tools
|
|
113
121
|
|
|
@@ -35,6 +35,7 @@ module RobotLab
|
|
|
35
35
|
# Apply a prompt_manager template to the persistent chat.
|
|
36
36
|
# If required parameters are missing, applies front matter config but
|
|
37
37
|
# defers rendering until run time when all values are available.
|
|
38
|
+
# :reek:TooManyStatements -- linear parse/merge/apply/render sequence with a documented deferred-render rescue.
|
|
38
39
|
def apply_template_to_chat(context)
|
|
39
40
|
parsed = PM.parse(@template)
|
|
40
41
|
|
|
@@ -68,6 +69,7 @@ module RobotLab
|
|
|
68
69
|
# Re-rendering replaces the system message, so the inline system_prompt must be
|
|
69
70
|
# re-appended here exactly as apply_system_prompt does at construction --
|
|
70
71
|
# otherwise it would be silently dropped on any run that supplies context.
|
|
72
|
+
# :reek:TooManyStatements -- must rebuild skills + template + inline prompt in one pass (see comment above).
|
|
71
73
|
def rerender_template(run_context)
|
|
72
74
|
merged = (@build_context || {}).merge(run_context)
|
|
73
75
|
resolved_ctx = resolve_context(merged, network: nil)
|
|
@@ -116,6 +118,7 @@ module RobotLab
|
|
|
116
118
|
# Pure computation — reads ivars but does not mutate @chat.
|
|
117
119
|
#
|
|
118
120
|
# @return [Array(Array<String>, RunConfig, Hash)] bodies, merged config, extras hash
|
|
121
|
+
# :reek:TooManyStatements -- accumulates bodies/config/extras across skills then the main template in one pure pass.
|
|
119
122
|
def collect_prompt_content(skill_ids, context)
|
|
120
123
|
visited = Set.new
|
|
121
124
|
visited.add(@template) if @template
|
|
@@ -128,16 +131,18 @@ module RobotLab
|
|
|
128
131
|
|
|
129
132
|
@expanded_skills.each do |skill_id|
|
|
130
133
|
parsed = PM.parse(skill_id)
|
|
131
|
-
|
|
132
|
-
|
|
134
|
+
metadata = parsed.metadata
|
|
135
|
+
accumulate_extras(metadata, extras)
|
|
136
|
+
accumulated_config = accumulated_config.merge(RunConfig.from_front_matter(metadata))
|
|
133
137
|
body = render_body(parsed, resolved_ctx)
|
|
134
138
|
bodies << body if body
|
|
135
139
|
end
|
|
136
140
|
|
|
137
141
|
if @template
|
|
138
142
|
parsed = PM.parse(@template)
|
|
139
|
-
|
|
140
|
-
|
|
143
|
+
metadata = parsed.metadata
|
|
144
|
+
accumulate_extras(metadata, extras)
|
|
145
|
+
accumulated_config = accumulated_config.merge(RunConfig.from_front_matter(metadata))
|
|
141
146
|
body = render_body(parsed, resolved_ctx)
|
|
142
147
|
bodies << body if body
|
|
143
148
|
end
|
|
@@ -181,6 +186,7 @@ module RobotLab
|
|
|
181
186
|
# @param visited [Set<Symbol>] already-visited IDs for cycle detection
|
|
182
187
|
# @param catalog [AgentSkillCatalog] catalog to check first
|
|
183
188
|
# @return [Array<Symbol>] flat ordered list of PM-based skill IDs
|
|
189
|
+
# :reek:TooManyStatements -- depth-first skill expansion with cycle guard and catalog-vs-PM branching.
|
|
184
190
|
def expand_skills_with_catalog(skill_ids, visited, catalog)
|
|
185
191
|
result = []
|
|
186
192
|
|
|
@@ -226,6 +232,7 @@ module RobotLab
|
|
|
226
232
|
#
|
|
227
233
|
# @param metadata [PM::Metadata] front matter metadata
|
|
228
234
|
# @return [Array<Symbol>]
|
|
235
|
+
# :reek:FeatureEnvy -- reading the metadata argument's skills list is the extraction itself.
|
|
229
236
|
def extract_skills_from_metadata(metadata)
|
|
230
237
|
return [] unless metadata.respond_to?(:skills) && metadata.skills
|
|
231
238
|
|
|
@@ -276,6 +283,7 @@ module RobotLab
|
|
|
276
283
|
|
|
277
284
|
# Extract identity and capability keys from front matter metadata.
|
|
278
285
|
# Constructor-provided values take precedence over frontmatter.
|
|
286
|
+
# :reek:FeatureEnvy -- copying front-matter metadata fields into this robot's ivars is the method's purpose.
|
|
279
287
|
def apply_front_matter_extras(metadata)
|
|
280
288
|
if metadata.respond_to?(:robot_name) && metadata.robot_name && !@name_from_constructor
|
|
281
289
|
@name = metadata.robot_name.to_s
|
|
@@ -309,6 +317,7 @@ module RobotLab
|
|
|
309
317
|
# Resolve string tool names from frontmatter to Ruby constants.
|
|
310
318
|
# Tool subclasses are instantiated; instances are used as-is.
|
|
311
319
|
# Unresolvable names are skipped with a warning.
|
|
320
|
+
# :reek:FeatureEnvy -- inspecting each resolved constant to decide instantiate-vs-use-as-is.
|
|
312
321
|
def resolve_frontmatter_tools(tool_names)
|
|
313
322
|
tool_names.filter_map do |name|
|
|
314
323
|
case name
|