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/robot.rb
CHANGED
|
@@ -41,6 +41,12 @@ module RobotLab
|
|
|
41
41
|
# local_tools: [OrderLookup, RefundProcessor]
|
|
42
42
|
# )
|
|
43
43
|
#
|
|
44
|
+
# :reek:TooManyMethods :reek:TooManyInstanceVariables -- Robot is intentionally the central orchestrator
|
|
45
|
+
# (see CLAUDE.md); behavior is split into the included modules below.
|
|
46
|
+
# :reek:InstanceVariableAssumption -- every ivar is assigned in #initialize via assign_identity_ivars/
|
|
47
|
+
# extract_config_ivars/initialize_runtime_state/initialize_memory; reek does not trace those helpers.
|
|
48
|
+
# :reek:RepeatedConditional -- `@bus` presence gates optional bus wiring at each independent site.
|
|
49
|
+
# :reek:DataClump -- (context, system_prompt, template) travel together as the robot's identity triple across construction APIs.
|
|
44
50
|
class Robot < RubyLLM::Agent
|
|
45
51
|
include Robot::TemplateRendering
|
|
46
52
|
include Robot::MCPManagement
|
|
@@ -155,6 +161,8 @@ module RobotLab
|
|
|
155
161
|
# @param stop [String, Array, nil] stop sequences
|
|
156
162
|
# @param skills [Symbol, Array<Symbol>, nil] skill templates to prepend
|
|
157
163
|
# @param config [RunConfig, nil] shared configuration (merged with explicit kwargs)
|
|
164
|
+
# :reek:LongParameterList -- the documented public constructor: one keyword per robot capability.
|
|
165
|
+
# :reek:BooleanParameter -- enable_cache and mcp_discovery are documented feature toggles.
|
|
158
166
|
def initialize(
|
|
159
167
|
name:,
|
|
160
168
|
template: nil,
|
|
@@ -230,6 +238,7 @@ module RobotLab
|
|
|
230
238
|
# Returns the model identifier
|
|
231
239
|
#
|
|
232
240
|
# @return [String, nil] the LLM model ID string
|
|
241
|
+
# :reek:FeatureEnvy -- duck-type unwrapping of the chat's model object into an id string.
|
|
233
242
|
def model
|
|
234
243
|
return nil unless @chat.respond_to?(:model)
|
|
235
244
|
|
|
@@ -245,6 +254,8 @@ module RobotLab
|
|
|
245
254
|
# @param model [String, nil] new model
|
|
246
255
|
# @param temperature [Float, nil] new temperature
|
|
247
256
|
# @return [self]
|
|
257
|
+
# :reek:LongParameterList :reek:TooManyStatements -- public reconfiguration API; one guarded with_* application per option.
|
|
258
|
+
# :reek:ControlParameter -- `context || @build_context` is a fallback default, not behavior selection.
|
|
248
259
|
def update(template: nil, context: nil, system_prompt: nil, model: nil, temperature: nil, **kwargs)
|
|
249
260
|
if template
|
|
250
261
|
@template = template
|
|
@@ -268,6 +279,8 @@ module RobotLab
|
|
|
268
279
|
#
|
|
269
280
|
# @param result [SimpleFlow::Result] incoming result from previous step
|
|
270
281
|
# @return [SimpleFlow::Result] result with robot output
|
|
282
|
+
# :reek:TooManyStatements -- SimpleFlow step: timing, run, and error-shielding belong together.
|
|
283
|
+
# :reek:DuplicateMethodCall -- each clock_gettime samples a different instant (start/stop/rescue); a local would be wrong.
|
|
271
284
|
def call(result)
|
|
272
285
|
run_context = extract_run_context(result)
|
|
273
286
|
|
|
@@ -369,6 +382,7 @@ module RobotLab
|
|
|
369
382
|
#
|
|
370
383
|
# @param keep_system [Boolean] whether to preserve the system message
|
|
371
384
|
# @return [self]
|
|
385
|
+
# :reek:BooleanParameter :reek:ControlParameter -- keep_system is a documented public API toggle.
|
|
372
386
|
def clear_messages(keep_system: true)
|
|
373
387
|
if keep_system
|
|
374
388
|
system_msg = @chat.messages.find { |m| m.role == :system }
|
|
@@ -448,6 +462,10 @@ module RobotLab
|
|
|
448
462
|
# @param kwargs [Hash] additional keyword args forwarded to Robot#run
|
|
449
463
|
# @return [RobotResult] when async: false
|
|
450
464
|
# @return [DelegationFuture] when async: true
|
|
465
|
+
# :reek:BooleanParameter :reek:ControlParameter -- async is the documented sync/future API switch.
|
|
466
|
+
# :reek:TooManyStatements -- the sync and async timing/annotation paths read best side by side.
|
|
467
|
+
# :reek:DuplicateMethodCall -- clock_gettime pairs sample distinct start/stop instants in each branch.
|
|
468
|
+
# :reek:FeatureEnvy -- annotating the delegatee's result with duration and delegator is the point of delegate.
|
|
451
469
|
def delegate(to:, task:, async: false, **)
|
|
452
470
|
if async
|
|
453
471
|
future = DelegationFuture.new(robot_name: to.name, delegated_by: @name)
|
|
@@ -478,6 +496,7 @@ module RobotLab
|
|
|
478
496
|
# into chat internals.
|
|
479
497
|
#
|
|
480
498
|
# @return [String, nil]
|
|
499
|
+
# :reek:FeatureEnvy -- duck-type unwrapping of the chat's model object.
|
|
481
500
|
def chat_provider
|
|
482
501
|
m = @chat.model
|
|
483
502
|
m.respond_to?(:provider) ? m.provider : nil
|
|
@@ -504,6 +523,7 @@ module RobotLab
|
|
|
504
523
|
#
|
|
505
524
|
# @param text [String] the insight to record
|
|
506
525
|
# @return [self]
|
|
526
|
+
# :reek:TooManyStatements -- dedupe/store/hook sequence shares the learnings state throughout.
|
|
507
527
|
def learn(text)
|
|
508
528
|
text = text.to_s.strip
|
|
509
529
|
return self if text.empty?
|
|
@@ -561,6 +581,7 @@ module RobotLab
|
|
|
561
581
|
# `tools:` is a NAME allowlist (a filter over available tools), not a place
|
|
562
582
|
# to attach tool instances — passing instances there silently attaches
|
|
563
583
|
# nothing. Catch the mistake with a clear, actionable error.
|
|
584
|
+
# :reek:FeatureEnvy -- inspecting each offending entry to build an actionable error message.
|
|
564
585
|
def validate_tools_filter!(tools)
|
|
565
586
|
return unless tools.is_a?(Array)
|
|
566
587
|
|
|
@@ -574,6 +595,8 @@ module RobotLab
|
|
|
574
595
|
"(e.g. RobotLab.build(local_tools: [MyTool.new]))."
|
|
575
596
|
end
|
|
576
597
|
|
|
598
|
+
# :reek:LongParameterList :reek:TooManyStatements -- one keyword and one assignment per identity ivar;
|
|
599
|
+
# a hash would lose keyword checking.
|
|
577
600
|
def assign_identity_ivars(name:, template:, system_prompt:, context:, description:,
|
|
578
601
|
local_tools:, skills:, mcp_discovery:)
|
|
579
602
|
@name = name.to_s
|
|
@@ -592,6 +615,8 @@ module RobotLab
|
|
|
592
615
|
|
|
593
616
|
# Build RunConfig from explicit kwargs, merged on top of any passed-in config.
|
|
594
617
|
# Explicit constructor kwargs always win.
|
|
618
|
+
# :reek:LongParameterList -- mirrors the constructor's LLM kwargs one-to-one so explicit values win over config.
|
|
619
|
+
# :reek:FeatureEnvy -- assembling the explicit_fields hash it just built is the merge itself.
|
|
595
620
|
def build_effective_config(model:, temperature:, top_p:, top_k:, max_tokens:,
|
|
596
621
|
presence_penalty:, frequency_penalty:, stop:,
|
|
597
622
|
on_tool_call:, on_tool_result:, on_content:,
|
|
@@ -710,6 +735,7 @@ module RobotLab
|
|
|
710
735
|
|
|
711
736
|
# Dynamically delegate all with_* methods from @chat, returning self for chaining.
|
|
712
737
|
# Discovered from the actual Chat class to avoid maintenance sync issues.
|
|
738
|
+
# :reek:NestedIterators -- the inner block is the delegator method body being defined, not an iteration.
|
|
713
739
|
def define_chat_delegators
|
|
714
740
|
@chat.class.public_instance_methods(false)
|
|
715
741
|
.select { |m| m.start_with?('with_') }
|
|
@@ -721,6 +747,7 @@ module RobotLab
|
|
|
721
747
|
end
|
|
722
748
|
end
|
|
723
749
|
|
|
750
|
+
# :reek:ControlParameter -- `network_memory || network&.memory || @memory` is the documented memory cascade.
|
|
724
751
|
def resolve_active_memory(network: nil, network_memory: nil)
|
|
725
752
|
network_memory || network&.memory || @memory
|
|
726
753
|
end
|
|
@@ -782,13 +809,15 @@ module RobotLab
|
|
|
782
809
|
#
|
|
783
810
|
# @param tools [Array<Tool>] the resolved tools
|
|
784
811
|
# @return [Array<Tool>] at most `max_tools` tools
|
|
812
|
+
# :reek:FeatureEnvy -- clamping the passed-in tool list against the provider limit is a pure filter.
|
|
785
813
|
def cap_tools(tools)
|
|
786
|
-
max
|
|
787
|
-
|
|
814
|
+
max = effective_max_tools
|
|
815
|
+
size = tools.size
|
|
816
|
+
return tools if max.nil? || size <= max
|
|
788
817
|
|
|
789
818
|
RobotLab.config.logger.warn(
|
|
790
|
-
"[#{@name}] tool list (#{
|
|
791
|
-
"sending #{max}, dropping #{
|
|
819
|
+
"[#{@name}] tool list (#{size}) exceeds max_tools (#{max}); " \
|
|
820
|
+
"sending #{max}, dropping #{size - max}"
|
|
792
821
|
)
|
|
793
822
|
tools.first(max)
|
|
794
823
|
end
|
|
@@ -802,10 +831,12 @@ module RobotLab
|
|
|
802
831
|
configured&.positive? ? configured : DEFAULT_MAX_TOOLS
|
|
803
832
|
end
|
|
804
833
|
|
|
834
|
+
# :reek:TooManyStatements -- builds the generation hook context then runs the pre-ask setup chain in order.
|
|
805
835
|
def invoke_ask(context:, kwargs:, hooks:, block:)
|
|
836
|
+
network = context.network
|
|
806
837
|
generation_context = LlmGenerationHookContext.new(
|
|
807
838
|
robot: self,
|
|
808
|
-
network:
|
|
839
|
+
network: network,
|
|
809
840
|
task: context.task,
|
|
810
841
|
memory: context.memory,
|
|
811
842
|
config: context.config,
|
|
@@ -814,9 +845,9 @@ module RobotLab
|
|
|
814
845
|
)
|
|
815
846
|
|
|
816
847
|
RobotLab::Hooks.run(:llm_generation, generation_context,
|
|
817
|
-
registries: hook_registries(
|
|
848
|
+
registries: hook_registries(network), per_run_hooks: hooks) do
|
|
818
849
|
effective_message = inject_learnings(generation_context.request)
|
|
819
|
-
maybe_compact(network:
|
|
850
|
+
maybe_compact(network: network)
|
|
820
851
|
install_circuit_breaker if @config.max_tool_rounds
|
|
821
852
|
install_doom_loop_detection
|
|
822
853
|
ask_kwargs = kwargs.slice(:with)
|
|
@@ -830,6 +861,7 @@ module RobotLab
|
|
|
830
861
|
end
|
|
831
862
|
|
|
832
863
|
# Extract run context from SimpleFlow::Result
|
|
864
|
+
# :reek:TooManyStatements -- one delete/re-add per robot-specific run param; a loop would hide which keys are special.
|
|
833
865
|
def extract_run_context(result)
|
|
834
866
|
run_params = (result.context[:run_params] || {}).dup
|
|
835
867
|
|
|
@@ -846,15 +878,16 @@ module RobotLab
|
|
|
846
878
|
base = run_params.dup
|
|
847
879
|
|
|
848
880
|
# Merge current value into context
|
|
849
|
-
|
|
881
|
+
value = result.value
|
|
882
|
+
merged = case value
|
|
850
883
|
when Hash
|
|
851
|
-
base.merge(
|
|
884
|
+
base.merge(value.transform_keys(&:to_sym))
|
|
852
885
|
when RobotResult
|
|
853
|
-
base.merge(message:
|
|
886
|
+
base.merge(message: value.last_text_content)
|
|
854
887
|
when String
|
|
855
|
-
base.merge(message:
|
|
888
|
+
base.merge(message: value)
|
|
856
889
|
else
|
|
857
|
-
base.merge(message:
|
|
890
|
+
base.merge(message: value.to_s)
|
|
858
891
|
end
|
|
859
892
|
|
|
860
893
|
# Add back the special params
|
|
@@ -869,6 +902,8 @@ module RobotLab
|
|
|
869
902
|
merged
|
|
870
903
|
end
|
|
871
904
|
|
|
905
|
+
# :reek:TooManyStatements :reek:FeatureEnvy -- adapting a provider response's many optional fields into
|
|
906
|
+
# a RobotResult is inherently response-centric.
|
|
872
907
|
def build_result(response, _memory)
|
|
873
908
|
text = result_text(response)
|
|
874
909
|
output = text ? [TextMessage.new(role: 'assistant', content: text)] : []
|
|
@@ -876,11 +911,10 @@ module RobotLab
|
|
|
876
911
|
tool_calls = response.respond_to?(:tool_calls) ? (response.tool_calls || []) : []
|
|
877
912
|
|
|
878
913
|
# Extract token usage from the response
|
|
879
|
-
input_toks = 0
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
output_toks = response.tokens.output.to_i
|
|
914
|
+
input_toks = output_toks = 0
|
|
915
|
+
if response.respond_to?(:tokens) && (tokens = response.tokens)
|
|
916
|
+
input_toks = tokens.input.to_i
|
|
917
|
+
output_toks = tokens.output.to_i
|
|
884
918
|
elsif response.respond_to?(:input_tokens)
|
|
885
919
|
input_toks = response.input_tokens.to_i
|
|
886
920
|
output_toks = response.respond_to?(:output_tokens) ? response.output_tokens.to_i : 0
|
|
@@ -909,6 +943,7 @@ module RobotLab
|
|
|
909
943
|
# The chat-history fallback is scoped to messages AFTER the last user message
|
|
910
944
|
# (the current turn) to prevent a previous turn's response from being returned
|
|
911
945
|
# when a thinking-mode model emits nothing in response.content.
|
|
946
|
+
# :reek:TooManyStatements :reek:FeatureEnvy -- documented fallback chain over the response's optional content/thinking/history fields.
|
|
912
947
|
def result_text(response)
|
|
913
948
|
content = response.content if response.respond_to?(:content)
|
|
914
949
|
return content if content && !content.to_s.empty?
|
|
@@ -987,6 +1022,8 @@ module RobotLab
|
|
|
987
1022
|
# Tracks tool call names; when a consecutive or cyclic repetition exceeds
|
|
988
1023
|
# the threshold, embeds a self-correction warning in the tool result so the
|
|
989
1024
|
# LLM can change strategy without requiring an external circuit breaker.
|
|
1025
|
+
# :reek:TooManyStatements :reek:FeatureEnvy -- the singleton override closes over the detector;
|
|
1026
|
+
# tracking/checking it there is the design.
|
|
990
1027
|
def install_doom_loop_detection
|
|
991
1028
|
threshold = @config.doom_loop_threshold || DoomLoopDetector::DEFAULT_THRESHOLD
|
|
992
1029
|
detector = DoomLoopDetector.new(threshold: threshold)
|
|
@@ -1010,6 +1047,7 @@ module RobotLab
|
|
|
1010
1047
|
end
|
|
1011
1048
|
|
|
1012
1049
|
# Remove the doom loop detection singleton method from @chat.
|
|
1050
|
+
# :reek:FeatureEnvy -- checking-then-removing on the chat's singleton class is one atomic operation.
|
|
1013
1051
|
def remove_doom_loop_detection
|
|
1014
1052
|
sc = @chat.singleton_class
|
|
1015
1053
|
sc.remove_method(:execute_tool) if sc.method_defined?(:execute_tool)
|
|
@@ -1025,6 +1063,9 @@ module RobotLab
|
|
|
1025
1063
|
# Fires the :compaction hook family (before/around/after_compaction).
|
|
1026
1064
|
# An on_compaction handler can replace the default strategy entirely by
|
|
1027
1065
|
# setting ctx.compacted_messages; the core algorithm is skipped when handled.
|
|
1066
|
+
# :reek:TooManyStatements -- guard chain plus hook-wrapped strategy dispatch; splitting would separate the hook from its guards.
|
|
1067
|
+
# :reek:DuplicateMethodCall -- @chat.messages is read at different lifecycle points
|
|
1068
|
+
# (guard, before-snapshot, after-snapshot); it mutates in between.
|
|
1028
1069
|
def maybe_compact(network: nil)
|
|
1029
1070
|
return if @chat.messages.empty?
|
|
1030
1071
|
|
|
@@ -60,6 +60,8 @@ module RobotLab
|
|
|
60
60
|
# @param stop_reason [String, nil] reason for stopping
|
|
61
61
|
# @param input_tokens [Integer] input tokens consumed (default 0)
|
|
62
62
|
# @param output_tokens [Integer] output tokens generated (default 0)
|
|
63
|
+
# :reek:LongParameterList -- serializable value object: one keyword per persisted field.
|
|
64
|
+
# :reek:ControlParameter -- `created_at || Time.now` and `id || SecureRandom.uuid` are nil-safe defaults.
|
|
63
65
|
def initialize(
|
|
64
66
|
robot_name:,
|
|
65
67
|
output:,
|
|
@@ -178,6 +180,7 @@ module RobotLab
|
|
|
178
180
|
# @param hash [Hash] Serialized result data
|
|
179
181
|
# @return [RobotResult]
|
|
180
182
|
#
|
|
183
|
+
# :reek:DuplicateMethodCall -- Message.from_hash maps four distinct message collections; there is no shared receiver to hoist.
|
|
181
184
|
def self.from_hash(hash)
|
|
182
185
|
hash = hash.transform_keys(&:to_sym)
|
|
183
186
|
|
data/lib/robot_lab/run_config.rb
CHANGED
|
@@ -27,6 +27,7 @@ module RobotLab
|
|
|
27
27
|
# effective.temperature #=> 0.9
|
|
28
28
|
# effective.model #=> "claude-sonnet-4"
|
|
29
29
|
#
|
|
30
|
+
# :reek:RepeatedConditional -- `if value` guards in independent field loops; each skips unset fields.
|
|
30
31
|
class RunConfig
|
|
31
32
|
# LLM configuration fields (applied to chat via with_* methods)
|
|
32
33
|
LLM_FIELDS = %i[
|
|
@@ -119,6 +120,8 @@ module RobotLab
|
|
|
119
120
|
# @param chat [Object] a RubyLLM::Chat (or similar) that responds to with_model, with_temperature, etc.
|
|
120
121
|
# @param provider [String, Symbol, nil] passed through to chat.with_model's provider: kwarg
|
|
121
122
|
# @param assume_model_exists [Boolean] passed through to chat.with_model's assume_exists: kwarg
|
|
123
|
+
# :reek:BooleanParameter -- assume_model_exists is a pass-through to RubyLLM's with_model kwarg.
|
|
124
|
+
# :reek:FeatureEnvy -- configuring the chat object handed in is exactly what apply_to is for.
|
|
122
125
|
def apply_to(chat, provider: nil, assume_model_exists: false)
|
|
123
126
|
LLM_FIELDS.each do |field|
|
|
124
127
|
value = @fields[field]
|
|
@@ -141,6 +144,7 @@ module RobotLab
|
|
|
141
144
|
#
|
|
142
145
|
# @param metadata [Object] a PM::Metadata object (responds to field names)
|
|
143
146
|
# @return [RunConfig]
|
|
147
|
+
# :reek:TooManyStatements -- linear duck-typed extraction of LLM then tool fields from front matter.
|
|
144
148
|
def self.from_front_matter(metadata)
|
|
145
149
|
fields = {}
|
|
146
150
|
|
|
@@ -171,6 +175,7 @@ module RobotLab
|
|
|
171
175
|
|
|
172
176
|
# @param other [RunConfig] the other RunConfig to compare
|
|
173
177
|
# @return [Boolean]
|
|
178
|
+
# :reek:FeatureEnvy -- equality naturally reads the other operand.
|
|
174
179
|
def ==(other)
|
|
175
180
|
other.is_a?(RunConfig) && to_h == other.to_h
|
|
176
181
|
end
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require 'open3'
|
|
4
4
|
require 'shellwords'
|
|
5
|
-
require 'timeout'
|
|
6
5
|
|
|
7
6
|
module RobotLab
|
|
8
7
|
# Factory module for wrapping AgentSkills scripts as RobotLab::Tool instances.
|
|
@@ -10,7 +9,18 @@ module RobotLab
|
|
|
10
9
|
# Given a path to an executable shell script, produces a Tool that shells
|
|
11
10
|
# out to the script and returns its combined stdout+stderr output.
|
|
12
11
|
# Non-executable scripts return nil with a logged warning.
|
|
12
|
+
#
|
|
13
|
+
# Core has no sandboxing of its own: by default every script runs unconfined
|
|
14
|
+
# with no timeout. An extension gem (e.g. robot_lab-sandbox) can install a
|
|
15
|
+
# confinement strategy by setting {.executor} to an object responding to
|
|
16
|
+
# +call(cmd, capabilities:, skill_dir:)+; when set, ScriptTool.execute
|
|
17
|
+
# delegates to it instead of running the command directly.
|
|
13
18
|
module ScriptTool
|
|
19
|
+
class << self
|
|
20
|
+
# @return [#call, nil] optional executor installed by an extension gem
|
|
21
|
+
attr_accessor :executor
|
|
22
|
+
end
|
|
23
|
+
|
|
14
24
|
# Wrap a script file as a RobotLab::Tool.
|
|
15
25
|
#
|
|
16
26
|
# @param script_path [String, Pathname] path to the script file
|
|
@@ -19,6 +29,8 @@ module RobotLab
|
|
|
19
29
|
# @param capabilities [Capabilities, nil] declared capabilities (from SKILL.md)
|
|
20
30
|
# @param skill_dir [String, nil] skill bundle root (defaults to the script's dir)
|
|
21
31
|
# @return [RobotLab::Tool, nil] nil if the script is not executable
|
|
32
|
+
# :reek:ControlParameter -- `capabilities || ...` and `skill_dir || ...` are nil-safe defaults, not behavior selection.
|
|
33
|
+
# :reek:TooManyStatements -- linear derive/validate/build factory; the closure needs every derived local.
|
|
22
34
|
def self.from_path(script_path, capabilities: nil, skill_dir: nil)
|
|
23
35
|
path = Pathname.new(script_path)
|
|
24
36
|
|
|
@@ -52,49 +64,18 @@ module RobotLab
|
|
|
52
64
|
end
|
|
53
65
|
end
|
|
54
66
|
|
|
55
|
-
# Run a command,
|
|
67
|
+
# Run a command, delegating to the installed {.executor} if one is present.
|
|
56
68
|
#
|
|
57
|
-
#
|
|
58
|
-
# capture2e path with no timeout
|
|
59
|
-
#
|
|
60
|
-
# bounded by the grant's timeout.
|
|
69
|
+
# With no executor installed (the default — core has no sandboxing), this
|
|
70
|
+
# is a plain, unconfined capture2e path with no timeout. An extension gem
|
|
71
|
+
# that sets {.executor} controls confinement and timeout behavior entirely.
|
|
61
72
|
#
|
|
62
73
|
# @return [String] combined stdout+stderr, or an error string on failure
|
|
63
74
|
def self.execute(cmd, capabilities:, skill_dir:)
|
|
64
|
-
|
|
65
|
-
output, status = Open3.capture2e(*cmd)
|
|
66
|
-
return format_result(output, status)
|
|
67
|
-
end
|
|
75
|
+
return executor.call(cmd, capabilities: capabilities, skill_dir: skill_dir) if executor
|
|
68
76
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
begin
|
|
72
|
-
output, status = run_with_timeout(sandbox.wrap(cmd), grant.timeout)
|
|
73
|
-
format_result(output, status)
|
|
74
|
-
ensure
|
|
75
|
-
sandbox.cleanup
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# @return [Array(String, Process::Status|nil)] output and status (nil = timed out)
|
|
80
|
-
def self.run_with_timeout(cmd, timeout)
|
|
81
|
-
Open3.popen2e(*cmd, pgroup: true) do |stdin, out, wait|
|
|
82
|
-
stdin.close
|
|
83
|
-
output = +''
|
|
84
|
-
begin
|
|
85
|
-
Timeout.timeout(timeout) { output << out.read }
|
|
86
|
-
rescue Timeout::Error
|
|
87
|
-
terminate(wait.pid)
|
|
88
|
-
return ["#{output}\n[killed: exceeded #{timeout}s]", nil]
|
|
89
|
-
end
|
|
90
|
-
[output, wait.value]
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def self.terminate(pid)
|
|
95
|
-
Process.kill('-TERM', Process.getpgid(pid))
|
|
96
|
-
rescue StandardError
|
|
97
|
-
nil
|
|
77
|
+
output, status = Open3.capture2e(*cmd)
|
|
78
|
+
format_result(output, status)
|
|
98
79
|
end
|
|
99
80
|
|
|
100
81
|
# @param status [Process::Status, nil] nil indicates a timeout kill
|
|
@@ -147,6 +147,7 @@ module RobotLab
|
|
|
147
147
|
|
|
148
148
|
# Respond to method calls as hash access
|
|
149
149
|
#
|
|
150
|
+
# :reek:BooleanParameter -- include_private is Ruby's respond_to_missing? contract signature.
|
|
150
151
|
def respond_to_missing?(method_name, include_private = false)
|
|
151
152
|
key = method_name.to_s.chomp("=").to_sym
|
|
152
153
|
@data.key?(key) || super
|
|
@@ -36,6 +36,7 @@ module RobotLab
|
|
|
36
36
|
# @param publish [Proc] callback for publishing events
|
|
37
37
|
# @param parent_run_id [String, nil] parent run identifier
|
|
38
38
|
# @param sequence_counter [SequenceCounter, nil] shared sequence counter
|
|
39
|
+
# :reek:ControlParameter -- `sequence_counter || SequenceCounter.new` is a nil-safe default.
|
|
39
40
|
def initialize(run_id:, message_id:, scope:, publish:, parent_run_id: nil, sequence_counter: nil)
|
|
40
41
|
@run_id = run_id
|
|
41
42
|
@parent_run_id = parent_run_id
|
|
@@ -7,6 +7,7 @@ module RobotLab
|
|
|
7
7
|
# Defines the structure and types of events emitted during
|
|
8
8
|
# robot and network execution.
|
|
9
9
|
#
|
|
10
|
+
# :reek:TooManyConstants -- deliberately a flat catalog of every streaming event name.
|
|
10
11
|
module Events
|
|
11
12
|
# Run lifecycle events
|
|
12
13
|
RUN_STARTED = "run.started"
|
data/lib/robot_lab/task.rb
CHANGED
|
@@ -89,6 +89,7 @@ module RobotLab
|
|
|
89
89
|
|
|
90
90
|
private
|
|
91
91
|
|
|
92
|
+
# :reek:TooManyStatements -- one guarded assignment per task-level override injected into run params.
|
|
92
93
|
def enhanced_result(result)
|
|
93
94
|
run_params = deep_merge(result.context[:run_params] || {}, @context)
|
|
94
95
|
run_params[:mcp] = @mcp unless @mcp == :none
|
|
@@ -114,6 +115,7 @@ module RobotLab
|
|
|
114
115
|
# @param override [Hash] the overriding hash
|
|
115
116
|
# @return [Hash] the merged result
|
|
116
117
|
#
|
|
118
|
+
# :reek:FeatureEnvy -- pure recursive hash merge of its two arguments; it has no useful self.
|
|
117
119
|
def deep_merge(base, override)
|
|
118
120
|
base = base.transform_keys(&:to_sym)
|
|
119
121
|
override = override.transform_keys(&:to_sym)
|
data/lib/robot_lab/tool.rb
CHANGED
|
@@ -27,6 +27,8 @@ module RobotLab
|
|
|
27
27
|
# enabling tools that modify their robot's state (temperature,
|
|
28
28
|
# system prompt, spawning, etc.).
|
|
29
29
|
#
|
|
30
|
+
# :reek:InstanceVariableAssumption -- @custom_name/@mcp are set by Tool.create via instance_variable_set,
|
|
31
|
+
# @raise_on_error/@ractor_safe are class-level DSL ivars; every read is defined?-guarded.
|
|
30
32
|
class Tool < RubyLLM::Tool
|
|
31
33
|
# @!attribute [rw] robot
|
|
32
34
|
# @return [Robot, nil] the robot that owns this tool
|
|
@@ -93,6 +95,7 @@ module RobotLab
|
|
|
93
95
|
#
|
|
94
96
|
# @param args [Hash] the tool arguments from the LLM
|
|
95
97
|
# @return [Object] the tool result or an error string
|
|
98
|
+
# :reek:TooManyStatements -- hook-wrapped dispatch with per-error-class handling; the rescue clauses are the method.
|
|
96
99
|
def call(args)
|
|
97
100
|
context = ToolCallHookContext.new(tool: self, tool_args: args, robot: @robot)
|
|
98
101
|
|
|
@@ -145,6 +148,7 @@ module RobotLab
|
|
|
145
148
|
# mcp: "brave_search"
|
|
146
149
|
# ) { |args| mcp_client.call_tool("search", args) }
|
|
147
150
|
#
|
|
151
|
+
# :reek:TooManyStatements -- anonymous-class factory: DSL application and instance wiring form one linear build.
|
|
148
152
|
def self.create(name:, description: nil, parameters: nil, mcp: nil, robot: nil, &handler)
|
|
149
153
|
desc_text = description
|
|
150
154
|
params_hash = parameters
|
|
@@ -41,6 +41,7 @@ module RobotLab
|
|
|
41
41
|
# @param system_prompt [String, nil] additional system prompt
|
|
42
42
|
# @param metadata [Hash, nil] additional metadata
|
|
43
43
|
# @param id [String, nil] unique identifier (defaults to UUID)
|
|
44
|
+
# :reek:ControlParameter -- `metadata || {}` and `id || SecureRandom.uuid` are nil-safe defaults.
|
|
44
45
|
def initialize(content, session_id: nil, system_prompt: nil, metadata: nil, id: nil)
|
|
45
46
|
@content = content.to_s
|
|
46
47
|
@session_id = session_id
|
data/lib/robot_lab/utils.rb
CHANGED
|
@@ -13,6 +13,7 @@ module RobotLab
|
|
|
13
13
|
# When already inside an Async reactor, creates a child task.
|
|
14
14
|
# Otherwise, creates a temporary reactor that runs the block
|
|
15
15
|
# and cleans up automatically.
|
|
16
|
+
# :reek:FeatureEnvy -- formatting the rescued exception for the log line; there is nothing else to reference.
|
|
16
17
|
def dispatch_async(&block)
|
|
17
18
|
Async do
|
|
18
19
|
block.call
|
|
@@ -25,6 +26,7 @@ module RobotLab
|
|
|
25
26
|
#
|
|
26
27
|
# @param obj [Object] the object to duplicate
|
|
27
28
|
# @return [Object] the deep copy
|
|
29
|
+
# :reek:FeatureEnvy -- pure recursive copy of its argument; it has no useful self.
|
|
28
30
|
def deep_dup(obj)
|
|
29
31
|
case obj
|
|
30
32
|
when Hash
|
data/lib/robot_lab/version.rb
CHANGED
data/lib/robot_lab/waiter.rb
CHANGED
|
@@ -28,6 +28,9 @@ module RobotLab
|
|
|
28
28
|
# @param timeout [Numeric, nil] maximum seconds to wait (nil = indefinite)
|
|
29
29
|
# @return [Object, :timeout] the signaled value, or :timeout if timed out
|
|
30
30
|
#
|
|
31
|
+
# :reek:TooManyStatements -- register/block/drain steps of the wake-pipe protocol are one atomic unit.
|
|
32
|
+
# :reek:DuplicateMethodCall -- the synchronize blocks are deliberately separate: the lock must be released
|
|
33
|
+
# around the blocking wait_readable.
|
|
31
34
|
def wait(timeout: nil)
|
|
32
35
|
@mutex.synchronize do
|
|
33
36
|
return @value if @signaled
|
data/lib/robot_lab.rb
CHANGED
|
@@ -132,14 +132,15 @@ module RobotLab
|
|
|
132
132
|
end
|
|
133
133
|
|
|
134
134
|
def with_hook_scope(registries, per_run_hooks)
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
thread = Thread.current
|
|
136
|
+
previous = thread[:robot_lab_hook_scope]
|
|
137
|
+
thread[:robot_lab_hook_scope] = {
|
|
137
138
|
registries: registries,
|
|
138
139
|
per_run_hooks: per_run_hooks
|
|
139
140
|
}
|
|
140
141
|
yield
|
|
141
142
|
ensure
|
|
142
|
-
|
|
143
|
+
thread[:robot_lab_hook_scope] = previous
|
|
143
144
|
end
|
|
144
145
|
|
|
145
146
|
def current_hook_scope
|
|
@@ -233,6 +234,7 @@ module RobotLab
|
|
|
233
234
|
# name: "helper",
|
|
234
235
|
# system_prompt: "You are a helpful assistant."
|
|
235
236
|
# )
|
|
237
|
+
# :reek:LongParameterList :reek:BooleanParameter -- documented factory API mirroring Robot#initialize; enable_cache is a feature toggle.
|
|
236
238
|
def build(name: "robot", template: nil, system_prompt: nil, context: {}, enable_cache: true, bus: nil, skills: nil,
|
|
237
239
|
config: nil, **)
|
|
238
240
|
Robot.new(
|
|
@@ -295,6 +297,7 @@ module RobotLab
|
|
|
295
297
|
#
|
|
296
298
|
# @example Memory with caching disabled
|
|
297
299
|
# memory = RobotLab.create_memory(data: {}, enable_cache: false)
|
|
300
|
+
# :reek:BooleanParameter -- enable_cache is a documented feature toggle forwarded to Memory.new.
|
|
298
301
|
def create_memory(data: {}, enable_cache: true, **)
|
|
299
302
|
Memory.new(data: data, enable_cache: enable_cache, **)
|
|
300
303
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: robot_lab
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dewayne VanHoozer
|
|
@@ -197,8 +197,9 @@ description: |
|
|
|
197
197
|
memory; networks for coordinating multiple robots with intelligent routing; MCP (Model
|
|
198
198
|
Context Protocol) integration for external tool servers; and a memory system with Redis
|
|
199
199
|
backend and semantic caching. Optional gems add Rails integration (robot_lab-rails),
|
|
200
|
-
durable learning (robot_lab-durable), Ractor concurrency (robot_lab-ractor),
|
|
201
|
-
|
|
200
|
+
durable learning (robot_lab-durable), Ractor concurrency (robot_lab-ractor), document
|
|
201
|
+
storage (robot_lab-document_store), and OS-level skill-script confinement
|
|
202
|
+
(robot_lab-sandbox). Core itself imposes no execution limitations.
|
|
202
203
|
email:
|
|
203
204
|
- dvanhoozer@gmail.com
|
|
204
205
|
executables: []
|
|
@@ -218,12 +219,14 @@ files:
|
|
|
218
219
|
- ".loki"
|
|
219
220
|
- ".quality/reek_baseline.txt"
|
|
220
221
|
- ".rubocop.yml"
|
|
222
|
+
- Archspec.rb
|
|
221
223
|
- CHANGELOG.md
|
|
222
224
|
- CLAUDE.md
|
|
223
225
|
- COMMITS.md
|
|
224
226
|
- LICENSE.txt
|
|
225
227
|
- README.md
|
|
226
228
|
- Rakefile
|
|
229
|
+
- _typos.toml
|
|
227
230
|
- agent2agent_review.md
|
|
228
231
|
- agentf_improvements.md
|
|
229
232
|
- agents.md
|
|
@@ -492,9 +495,6 @@ files:
|
|
|
492
495
|
- lib/robot_lab/robot_result.rb
|
|
493
496
|
- lib/robot_lab/run_config.rb
|
|
494
497
|
- lib/robot_lab/runnable.rb
|
|
495
|
-
- lib/robot_lab/sandbox.rb
|
|
496
|
-
- lib/robot_lab/sandbox/null.rb
|
|
497
|
-
- lib/robot_lab/sandbox/seatbelt.rb
|
|
498
498
|
- lib/robot_lab/script_tool.rb
|
|
499
499
|
- lib/robot_lab/state_proxy.rb
|
|
500
500
|
- lib/robot_lab/streaming/context.rb
|
|
@@ -541,7 +541,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
541
541
|
- !ruby/object:Gem::Version
|
|
542
542
|
version: '0'
|
|
543
543
|
requirements: []
|
|
544
|
-
rubygems_version: 4.0.
|
|
544
|
+
rubygems_version: 4.0.20
|
|
545
545
|
specification_version: 4
|
|
546
546
|
summary: Ruby framework for building and orchestrating multi-robot LLM workflows
|
|
547
547
|
test_files: []
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RobotLab
|
|
4
|
-
module Sandbox
|
|
5
|
-
# Passthrough strategy: runs the command with no confinement. Used when
|
|
6
|
-
# sandboxing is unavailable (non-macOS) or unnecessary (trust: core).
|
|
7
|
-
class Null
|
|
8
|
-
def wrap(cmd) = cmd
|
|
9
|
-
|
|
10
|
-
def cleanup; end
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|