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.
Files changed (61) 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/Rakefile +6 -111
  7. data/_typos.toml +21 -0
  8. data/docs/api/index.md +2 -2
  9. data/docs/api/skills.md +53 -149
  10. data/docs/api/support.md +1 -1
  11. data/docs/architecture/index.md +5 -4
  12. data/docs/getting-started/configuration.md +4 -1
  13. data/docs/guides/hooks.md +122 -0
  14. data/docs/guides/using-tools.md +10 -1
  15. data/lib/robot_lab/agent_skill_catalog.rb +1 -0
  16. data/lib/robot_lab/ask_user.rb +2 -0
  17. data/lib/robot_lab/bus_poller.rb +2 -0
  18. data/lib/robot_lab/capabilities.rb +4 -0
  19. data/lib/robot_lab/config.rb +8 -0
  20. data/lib/robot_lab/doom_loop_detector.rb +6 -3
  21. data/lib/robot_lab/history_compressor.rb +5 -0
  22. data/lib/robot_lab/hook.rb +1 -0
  23. data/lib/robot_lab/hook_context.rb +4 -0
  24. data/lib/robot_lab/hook_registry.rb +1 -0
  25. data/lib/robot_lab/hooks.rb +6 -3
  26. data/lib/robot_lab/mcp/client.rb +2 -2
  27. data/lib/robot_lab/mcp/connection_poller.rb +16 -8
  28. data/lib/robot_lab/mcp/server_discovery.rb +1 -0
  29. data/lib/robot_lab/mcp/transports/sse.rb +3 -0
  30. data/lib/robot_lab/mcp/transports/stdio.rb +5 -0
  31. data/lib/robot_lab/mcp/transports/streamable_http.rb +4 -0
  32. data/lib/robot_lab/mcp/transports/websocket.rb +3 -0
  33. data/lib/robot_lab/memory.rb +23 -6
  34. data/lib/robot_lab/memory_change.rb +1 -0
  35. data/lib/robot_lab/message.rb +3 -0
  36. data/lib/robot_lab/names.rb +2 -4
  37. data/lib/robot_lab/network.rb +13 -6
  38. data/lib/robot_lab/robot/agent_skill_matching.rb +2 -0
  39. data/lib/robot_lab/robot/bus_messaging.rb +3 -0
  40. data/lib/robot_lab/robot/history_search.rb +2 -0
  41. data/lib/robot_lab/robot/hooking.rb +3 -0
  42. data/lib/robot_lab/robot/mcp_management.rb +11 -3
  43. data/lib/robot_lab/robot/template_rendering.rb +13 -4
  44. data/lib/robot_lab/robot.rb +58 -17
  45. data/lib/robot_lab/robot_result.rb +3 -0
  46. data/lib/robot_lab/run_config.rb +5 -0
  47. data/lib/robot_lab/script_tool.rb +20 -39
  48. data/lib/robot_lab/state_proxy.rb +1 -0
  49. data/lib/robot_lab/streaming/context.rb +1 -0
  50. data/lib/robot_lab/streaming/events.rb +1 -0
  51. data/lib/robot_lab/task.rb +2 -0
  52. data/lib/robot_lab/tool.rb +4 -0
  53. data/lib/robot_lab/user_message.rb +1 -0
  54. data/lib/robot_lab/utils.rb +2 -0
  55. data/lib/robot_lab/version.rb +1 -1
  56. data/lib/robot_lab/waiter.rb +3 -0
  57. data/lib/robot_lab.rb +6 -3
  58. metadata +7 -7
  59. data/lib/robot_lab/sandbox/null.rb +0 -13
  60. data/lib/robot_lab/sandbox/seatbelt.rb +0 -104
  61. data/lib/robot_lab/sandbox.rb +0 -52
@@ -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 = effective_max_tools
787
- return tools if max.nil? || tools.size <= max
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 (#{tools.size}) exceeds max_tools (#{max}); " \
791
- "sending #{max}, dropping #{tools.size - max}"
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: context.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(context.network), per_run_hooks: hooks) do
848
+ registries: hook_registries(network), per_run_hooks: hooks) do
818
849
  effective_message = inject_learnings(generation_context.request)
819
- maybe_compact(network: context.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
- merged = case result.value
881
+ value = result.value
882
+ merged = case value
850
883
  when Hash
851
- base.merge(result.value.transform_keys(&:to_sym))
884
+ base.merge(value.transform_keys(&:to_sym))
852
885
  when RobotResult
853
- base.merge(message: result.value.last_text_content)
886
+ base.merge(message: value.last_text_content)
854
887
  when String
855
- base.merge(message: result.value)
888
+ base.merge(message: value)
856
889
  else
857
- base.merge(message: result.value.to_s)
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
- output_toks = 0
881
- if response.respond_to?(:tokens) && response.tokens
882
- input_toks = response.tokens.input.to_i
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
 
@@ -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, optionally confined by the sandbox and a timeout.
67
+ # Run a command, delegating to the installed {.executor} if one is present.
56
68
  #
57
- # When sandboxing is disabled (the default) this is the original, unconfined
58
- # capture2e path with no timeout — behaviour is unchanged. When enabled, the
59
- # command is wrapped by the sandbox strategy for the effective grant and
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
- unless Sandbox.enabled?
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
- grant = capabilities.intersect(Capabilities.ceiling)
70
- sandbox = Sandbox.for(grant, skill_dir: skill_dir)
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"
@@ -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)
@@ -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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RobotLab
4
- VERSION = "0.2.7"
4
+ VERSION = "0.2.8"
5
5
  end
@@ -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
- previous = Thread.current[:robot_lab_hook_scope]
136
- Thread.current[:robot_lab_hook_scope] = {
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
- Thread.current[:robot_lab_hook_scope] = previous
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.7
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), and
201
- document storage (robot_lab-document_store).
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.19
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