robot_lab 0.2.6 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.envrc +4 -0
- data/.loki +5 -0
- data/Archspec.rb +44 -0
- data/CHANGELOG.md +17 -1
- data/README.md +124 -64
- data/Rakefile +6 -111
- data/_typos.toml +21 -0
- data/docs/api/core/index.md +41 -15
- data/docs/api/core/memory.md +247 -29
- data/docs/api/core/network.md +255 -33
- data/docs/api/core/result.md +120 -32
- data/docs/api/core/robot.md +551 -61
- data/docs/api/core/state.md +87 -197
- data/docs/api/core/tool.md +165 -20
- data/docs/api/errors.md +110 -17
- data/docs/api/hooks.md +469 -0
- data/docs/api/index.md +80 -7
- data/docs/api/mcp/client.md +129 -35
- data/docs/api/mcp/index.md +164 -23
- data/docs/api/mcp/server.md +27 -3
- data/docs/api/mcp/transports.md +94 -22
- data/docs/api/messages/index.md +26 -3
- data/docs/api/messages/text-message.md +33 -11
- data/docs/api/messages/tool-call-message.md +27 -4
- data/docs/api/messages/tool-result-message.md +23 -4
- data/docs/api/messages/user-message.md +45 -8
- data/docs/api/skills.md +423 -0
- data/docs/api/streaming/context.md +28 -5
- data/docs/api/streaming/index.md +57 -11
- data/docs/api/support.md +846 -0
- data/docs/architecture/core-concepts.md +79 -31
- data/docs/architecture/index.md +87 -11
- data/docs/architecture/message-flow.md +66 -29
- data/docs/architecture/network-orchestration.md +145 -38
- data/docs/architecture/robot-execution.md +172 -90
- data/docs/architecture/state-management.md +31 -12
- data/docs/concepts.md +176 -21
- data/docs/examples/basic-chat.md +72 -19
- data/docs/examples/index.md +117 -31
- data/docs/examples/mcp-server.md +154 -45
- data/docs/examples/multi-robot-network.md +91 -21
- data/docs/examples/tool-usage.md +104 -37
- data/docs/getting-started/configuration.md +288 -98
- data/docs/getting-started/installation.md +53 -41
- data/docs/getting-started/quick-start.md +51 -6
- data/docs/guides/building-robots.md +258 -50
- data/docs/guides/creating-networks.md +214 -30
- data/docs/guides/hooks.md +263 -54
- data/docs/guides/knowledge.md +35 -4
- data/docs/guides/mcp-integration.md +211 -44
- data/docs/guides/memory.md +103 -12
- data/docs/guides/observability.md +95 -47
- data/docs/guides/streaming.md +184 -125
- data/docs/guides/using-tools.md +247 -18
- data/docs/index.md +36 -4
- data/examples/01_simple_robot.rb +2 -2
- data/examples/02_tools.rb +14 -4
- data/examples/03_network.rb +12 -7
- data/examples/04_mcp.rb +11 -4
- data/examples/05_streaming.rb +8 -5
- data/examples/06_prompt_templates.rb +13 -9
- data/examples/07_network_memory.rb +5 -5
- data/examples/08_llm_config.rb +20 -15
- data/examples/09_chaining.rb +4 -4
- data/examples/11_network_introspection.rb +4 -4
- data/examples/12_message_bus.rb +2 -2
- data/examples/13_spawn.rb +2 -2
- data/examples/14_rusty_circuit/README.md +1 -0
- data/examples/14_rusty_circuit/comic.rb +7 -3
- data/examples/14_rusty_circuit/display.rb +14 -0
- data/examples/14_rusty_circuit/heckler.rb +8 -6
- data/examples/14_rusty_circuit/open_mic.rb +17 -6
- data/examples/14_rusty_circuit/scout.rb +17 -10
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +14 -10
- data/examples/15_memory_network_and_bus/linux_writer.rb +2 -2
- data/examples/15_memory_network_and_bus/os_editor.rb +3 -1
- data/examples/15_memory_network_and_bus/os_writer.rb +4 -1
- data/examples/16_writers_room/writer.rb +22 -22
- data/examples/16_writers_room/writers_room.rb +2 -0
- data/examples/17_skills.rb +14 -13
- data/examples/18_rails/README.md +20 -1
- data/examples/18_rails/app/controllers/chat_controller.rb +5 -1
- data/examples/18_rails/app/jobs/robot_run_job.rb +11 -5
- data/examples/18_rails/app/robots/chat_robot.rb +11 -0
- data/examples/18_rails/config/initializers/robot_lab.rb +8 -0
- data/examples/19_token_tracking.rb +25 -9
- data/examples/20_circuit_breaker.rb +10 -7
- data/examples/21_learning_loop.rb +42 -16
- data/examples/22_context_compression.rb +23 -23
- data/examples/23_convergence.rb +24 -17
- data/examples/24_structured_delegation.rb +13 -8
- data/examples/25_history_search.rb +12 -8
- data/examples/27_incident_response/incident_response.rb +31 -13
- data/examples/28_mcp_discovery.rb +17 -13
- data/examples/29_ractor_tools.rb +4 -2
- data/examples/30_ractor_network.rb +22 -17
- data/examples/31_launch_assessment.rb +20 -9
- data/examples/32_newsletter_reader.rb +7 -2
- data/examples/33_stock_predictor.rb +34 -13
- data/examples/34_agentskills.rb +7 -3
- data/examples/35_hooks.rb +18 -8
- data/examples/README.md +199 -45
- data/examples/common.rb +79 -11
- data/examples/xyzzy.rb +8 -1
- data/lib/robot_lab/agent_skill_catalog.rb +1 -0
- data/lib/robot_lab/ask_user.rb +2 -0
- data/lib/robot_lab/bus_poller.rb +2 -0
- data/lib/robot_lab/capabilities.rb +4 -0
- data/lib/robot_lab/config.rb +18 -5
- data/lib/robot_lab/doom_loop_detector.rb +6 -3
- data/lib/robot_lab/history_compressor.rb +5 -0
- data/lib/robot_lab/hook.rb +1 -0
- data/lib/robot_lab/hook_context.rb +4 -0
- data/lib/robot_lab/hook_registry.rb +1 -0
- data/lib/robot_lab/hooks.rb +6 -3
- data/lib/robot_lab/mcp/client.rb +2 -2
- data/lib/robot_lab/mcp/connection_poller.rb +16 -8
- data/lib/robot_lab/mcp/server_discovery.rb +1 -0
- data/lib/robot_lab/mcp/transports/sse.rb +3 -0
- data/lib/robot_lab/mcp/transports/stdio.rb +5 -0
- data/lib/robot_lab/mcp/transports/streamable_http.rb +4 -0
- data/lib/robot_lab/mcp/transports/websocket.rb +3 -0
- data/lib/robot_lab/memory.rb +23 -6
- data/lib/robot_lab/memory_change.rb +1 -0
- data/lib/robot_lab/message.rb +3 -0
- data/lib/robot_lab/names.rb +400 -0
- data/lib/robot_lab/network.rb +13 -6
- data/lib/robot_lab/robot/agent_skill_matching.rb +3 -3
- data/lib/robot_lab/robot/bus_messaging.rb +19 -8
- data/lib/robot_lab/robot/history_search.rb +2 -0
- data/lib/robot_lab/robot/hooking.rb +3 -0
- data/lib/robot_lab/robot/mcp_management.rb +11 -3
- data/lib/robot_lab/robot/template_rendering.rb +29 -7
- data/lib/robot_lab/robot.rb +81 -19
- data/lib/robot_lab/robot_result.rb +3 -0
- data/lib/robot_lab/run_config.rb +5 -0
- data/lib/robot_lab/script_tool.rb +20 -39
- data/lib/robot_lab/state_proxy.rb +1 -0
- data/lib/robot_lab/streaming/context.rb +1 -0
- data/lib/robot_lab/streaming/events.rb +1 -0
- data/lib/robot_lab/task.rb +2 -0
- data/lib/robot_lab/tool.rb +4 -0
- data/lib/robot_lab/user_message.rb +1 -0
- data/lib/robot_lab/utils.rb +2 -0
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab/waiter.rb +3 -0
- data/lib/robot_lab.rb +27 -18
- data/mkdocs.yml +6 -1
- metadata +12 -7
- data/lib/robot_lab/sandbox/null.rb +0 -13
- data/lib/robot_lab/sandbox/seatbelt.rb +0 -104
- data/lib/robot_lab/sandbox.rb +0 -52
|
@@ -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
|
|
|
@@ -64,6 +65,11 @@ module RobotLab
|
|
|
64
65
|
|
|
65
66
|
# Re-render the template with run-time context merged into build-time context.
|
|
66
67
|
# prompt_manager parameters may be required (null) and only available at run time.
|
|
68
|
+
#
|
|
69
|
+
# Re-rendering replaces the system message, so the inline system_prompt must be
|
|
70
|
+
# re-appended here exactly as apply_system_prompt does at construction --
|
|
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).
|
|
67
73
|
def rerender_template(run_context)
|
|
68
74
|
merged = (@build_context || {}).merge(run_context)
|
|
69
75
|
resolved_ctx = resolve_context(merged, network: nil)
|
|
@@ -81,15 +87,24 @@ module RobotLab
|
|
|
81
87
|
bodies << body if body
|
|
82
88
|
end
|
|
83
89
|
|
|
84
|
-
|
|
85
|
-
@chat.with_instructions(combined)
|
|
90
|
+
@chat.with_instructions(with_system_prompt_appended(bodies.join("\n\n")))
|
|
86
91
|
else
|
|
87
92
|
parsed = PM.parse(@template)
|
|
88
93
|
rendered = parsed.to_s(**resolved_ctx)
|
|
89
|
-
@chat.with_instructions(rendered)
|
|
94
|
+
@chat.with_instructions(with_system_prompt_appended(rendered))
|
|
90
95
|
end
|
|
91
96
|
end
|
|
92
97
|
|
|
98
|
+
# Append the inline system_prompt to freshly rendered template text.
|
|
99
|
+
#
|
|
100
|
+
# @param rendered [String] newly rendered template body
|
|
101
|
+
# @return [String] rendered text with system_prompt appended, if one is set
|
|
102
|
+
def with_system_prompt_appended(rendered)
|
|
103
|
+
return rendered unless @system_prompt
|
|
104
|
+
|
|
105
|
+
[rendered, @system_prompt].reject { |s| s.to_s.empty? }.join("\n\n")
|
|
106
|
+
end
|
|
107
|
+
|
|
93
108
|
# Orchestrate skill expansion and template application.
|
|
94
109
|
#
|
|
95
110
|
# @param skill_ids [Array<Symbol>] skill IDs from constructor + front matter
|
|
@@ -103,6 +118,7 @@ module RobotLab
|
|
|
103
118
|
# Pure computation — reads ivars but does not mutate @chat.
|
|
104
119
|
#
|
|
105
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.
|
|
106
122
|
def collect_prompt_content(skill_ids, context)
|
|
107
123
|
visited = Set.new
|
|
108
124
|
visited.add(@template) if @template
|
|
@@ -115,16 +131,18 @@ module RobotLab
|
|
|
115
131
|
|
|
116
132
|
@expanded_skills.each do |skill_id|
|
|
117
133
|
parsed = PM.parse(skill_id)
|
|
118
|
-
|
|
119
|
-
|
|
134
|
+
metadata = parsed.metadata
|
|
135
|
+
accumulate_extras(metadata, extras)
|
|
136
|
+
accumulated_config = accumulated_config.merge(RunConfig.from_front_matter(metadata))
|
|
120
137
|
body = render_body(parsed, resolved_ctx)
|
|
121
138
|
bodies << body if body
|
|
122
139
|
end
|
|
123
140
|
|
|
124
141
|
if @template
|
|
125
142
|
parsed = PM.parse(@template)
|
|
126
|
-
|
|
127
|
-
|
|
143
|
+
metadata = parsed.metadata
|
|
144
|
+
accumulate_extras(metadata, extras)
|
|
145
|
+
accumulated_config = accumulated_config.merge(RunConfig.from_front_matter(metadata))
|
|
128
146
|
body = render_body(parsed, resolved_ctx)
|
|
129
147
|
bodies << body if body
|
|
130
148
|
end
|
|
@@ -168,6 +186,7 @@ module RobotLab
|
|
|
168
186
|
# @param visited [Set<Symbol>] already-visited IDs for cycle detection
|
|
169
187
|
# @param catalog [AgentSkillCatalog] catalog to check first
|
|
170
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.
|
|
171
190
|
def expand_skills_with_catalog(skill_ids, visited, catalog)
|
|
172
191
|
result = []
|
|
173
192
|
|
|
@@ -213,6 +232,7 @@ module RobotLab
|
|
|
213
232
|
#
|
|
214
233
|
# @param metadata [PM::Metadata] front matter metadata
|
|
215
234
|
# @return [Array<Symbol>]
|
|
235
|
+
# :reek:FeatureEnvy -- reading the metadata argument's skills list is the extraction itself.
|
|
216
236
|
def extract_skills_from_metadata(metadata)
|
|
217
237
|
return [] unless metadata.respond_to?(:skills) && metadata.skills
|
|
218
238
|
|
|
@@ -263,6 +283,7 @@ module RobotLab
|
|
|
263
283
|
|
|
264
284
|
# Extract identity and capability keys from front matter metadata.
|
|
265
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.
|
|
266
287
|
def apply_front_matter_extras(metadata)
|
|
267
288
|
if metadata.respond_to?(:robot_name) && metadata.robot_name && !@name_from_constructor
|
|
268
289
|
@name = metadata.robot_name.to_s
|
|
@@ -296,6 +317,7 @@ module RobotLab
|
|
|
296
317
|
# Resolve string tool names from frontmatter to Ruby constants.
|
|
297
318
|
# Tool subclasses are instantiated; instances are used as-is.
|
|
298
319
|
# Unresolvable names are skipped with a warning.
|
|
320
|
+
# :reek:FeatureEnvy -- inspecting each resolved constant to decide instantiate-vs-use-as-is.
|
|
299
321
|
def resolve_frontmatter_tools(tool_names)
|
|
300
322
|
tool_names.filter_map do |name|
|
|
301
323
|
case name
|
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:,
|
|
@@ -666,8 +691,27 @@ module RobotLab
|
|
|
666
691
|
end
|
|
667
692
|
end
|
|
668
693
|
|
|
694
|
+
# Append the inline system prompt to whatever apply_template already set.
|
|
695
|
+
#
|
|
696
|
+
# The template renders first and establishes the base instructions; the
|
|
697
|
+
# inline system_prompt is appended so both survive. Combining into a single
|
|
698
|
+
# system message (rather than using with_instructions(append: true), which
|
|
699
|
+
# adds a second one) keeps readers that look at only the first system
|
|
700
|
+
# message -- notably AgentSkillMatching's snapshot/restore -- correct.
|
|
669
701
|
def apply_system_prompt
|
|
670
|
-
|
|
702
|
+
return unless @system_prompt
|
|
703
|
+
|
|
704
|
+
base = current_system_instructions.to_s
|
|
705
|
+
combined = [base, @system_prompt].reject(&:empty?).join("\n\n")
|
|
706
|
+
@chat.with_instructions(combined)
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
# Content of the chat's system message, or nil when none is set.
|
|
710
|
+
#
|
|
711
|
+
# @return [String, nil]
|
|
712
|
+
def current_system_instructions
|
|
713
|
+
messages = @chat.instance_variable_get(:@messages)
|
|
714
|
+
messages&.find { |m| m.role.to_s == "system" }&.content
|
|
671
715
|
end
|
|
672
716
|
|
|
673
717
|
def apply_chat_params
|
|
@@ -691,6 +735,7 @@ module RobotLab
|
|
|
691
735
|
|
|
692
736
|
# Dynamically delegate all with_* methods from @chat, returning self for chaining.
|
|
693
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.
|
|
694
739
|
def define_chat_delegators
|
|
695
740
|
@chat.class.public_instance_methods(false)
|
|
696
741
|
.select { |m| m.start_with?('with_') }
|
|
@@ -702,6 +747,7 @@ module RobotLab
|
|
|
702
747
|
end
|
|
703
748
|
end
|
|
704
749
|
|
|
750
|
+
# :reek:ControlParameter -- `network_memory || network&.memory || @memory` is the documented memory cascade.
|
|
705
751
|
def resolve_active_memory(network: nil, network_memory: nil)
|
|
706
752
|
network_memory || network&.memory || @memory
|
|
707
753
|
end
|
|
@@ -757,17 +803,21 @@ module RobotLab
|
|
|
757
803
|
# is the definitive choke point: tools are fully resolved (MCP connected)
|
|
758
804
|
# and about to be handed to the chat, so the cap holds no matter how the
|
|
759
805
|
# tools were configured, filtered, or connected. Cap value comes from
|
|
760
|
-
# RunConfig#max_tools, defaulting to DEFAULT_MAX_TOOLS; nil
|
|
806
|
+
# RunConfig#max_tools, defaulting to DEFAULT_MAX_TOOLS; a nil, zero, or
|
|
807
|
+
# negative max_tools falls back to that default, so the cap cannot be
|
|
808
|
+
# disabled from configuration.
|
|
761
809
|
#
|
|
762
810
|
# @param tools [Array<Tool>] the resolved tools
|
|
763
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.
|
|
764
813
|
def cap_tools(tools)
|
|
765
|
-
max
|
|
766
|
-
|
|
814
|
+
max = effective_max_tools
|
|
815
|
+
size = tools.size
|
|
816
|
+
return tools if max.nil? || size <= max
|
|
767
817
|
|
|
768
818
|
RobotLab.config.logger.warn(
|
|
769
|
-
"[#{@name}] tool list (#{
|
|
770
|
-
"sending #{max}, dropping #{
|
|
819
|
+
"[#{@name}] tool list (#{size}) exceeds max_tools (#{max}); " \
|
|
820
|
+
"sending #{max}, dropping #{size - max}"
|
|
771
821
|
)
|
|
772
822
|
tools.first(max)
|
|
773
823
|
end
|
|
@@ -781,10 +831,12 @@ module RobotLab
|
|
|
781
831
|
configured&.positive? ? configured : DEFAULT_MAX_TOOLS
|
|
782
832
|
end
|
|
783
833
|
|
|
834
|
+
# :reek:TooManyStatements -- builds the generation hook context then runs the pre-ask setup chain in order.
|
|
784
835
|
def invoke_ask(context:, kwargs:, hooks:, block:)
|
|
836
|
+
network = context.network
|
|
785
837
|
generation_context = LlmGenerationHookContext.new(
|
|
786
838
|
robot: self,
|
|
787
|
-
network:
|
|
839
|
+
network: network,
|
|
788
840
|
task: context.task,
|
|
789
841
|
memory: context.memory,
|
|
790
842
|
config: context.config,
|
|
@@ -793,9 +845,9 @@ module RobotLab
|
|
|
793
845
|
)
|
|
794
846
|
|
|
795
847
|
RobotLab::Hooks.run(:llm_generation, generation_context,
|
|
796
|
-
registries: hook_registries(
|
|
848
|
+
registries: hook_registries(network), per_run_hooks: hooks) do
|
|
797
849
|
effective_message = inject_learnings(generation_context.request)
|
|
798
|
-
maybe_compact(network:
|
|
850
|
+
maybe_compact(network: network)
|
|
799
851
|
install_circuit_breaker if @config.max_tool_rounds
|
|
800
852
|
install_doom_loop_detection
|
|
801
853
|
ask_kwargs = kwargs.slice(:with)
|
|
@@ -809,6 +861,7 @@ module RobotLab
|
|
|
809
861
|
end
|
|
810
862
|
|
|
811
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.
|
|
812
865
|
def extract_run_context(result)
|
|
813
866
|
run_params = (result.context[:run_params] || {}).dup
|
|
814
867
|
|
|
@@ -825,15 +878,16 @@ module RobotLab
|
|
|
825
878
|
base = run_params.dup
|
|
826
879
|
|
|
827
880
|
# Merge current value into context
|
|
828
|
-
|
|
881
|
+
value = result.value
|
|
882
|
+
merged = case value
|
|
829
883
|
when Hash
|
|
830
|
-
base.merge(
|
|
884
|
+
base.merge(value.transform_keys(&:to_sym))
|
|
831
885
|
when RobotResult
|
|
832
|
-
base.merge(message:
|
|
886
|
+
base.merge(message: value.last_text_content)
|
|
833
887
|
when String
|
|
834
|
-
base.merge(message:
|
|
888
|
+
base.merge(message: value)
|
|
835
889
|
else
|
|
836
|
-
base.merge(message:
|
|
890
|
+
base.merge(message: value.to_s)
|
|
837
891
|
end
|
|
838
892
|
|
|
839
893
|
# Add back the special params
|
|
@@ -848,6 +902,8 @@ module RobotLab
|
|
|
848
902
|
merged
|
|
849
903
|
end
|
|
850
904
|
|
|
905
|
+
# :reek:TooManyStatements :reek:FeatureEnvy -- adapting a provider response's many optional fields into
|
|
906
|
+
# a RobotResult is inherently response-centric.
|
|
851
907
|
def build_result(response, _memory)
|
|
852
908
|
text = result_text(response)
|
|
853
909
|
output = text ? [TextMessage.new(role: 'assistant', content: text)] : []
|
|
@@ -855,11 +911,10 @@ module RobotLab
|
|
|
855
911
|
tool_calls = response.respond_to?(:tool_calls) ? (response.tool_calls || []) : []
|
|
856
912
|
|
|
857
913
|
# Extract token usage from the response
|
|
858
|
-
input_toks = 0
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
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
|
|
863
918
|
elsif response.respond_to?(:input_tokens)
|
|
864
919
|
input_toks = response.input_tokens.to_i
|
|
865
920
|
output_toks = response.respond_to?(:output_tokens) ? response.output_tokens.to_i : 0
|
|
@@ -888,6 +943,7 @@ module RobotLab
|
|
|
888
943
|
# The chat-history fallback is scoped to messages AFTER the last user message
|
|
889
944
|
# (the current turn) to prevent a previous turn's response from being returned
|
|
890
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.
|
|
891
947
|
def result_text(response)
|
|
892
948
|
content = response.content if response.respond_to?(:content)
|
|
893
949
|
return content if content && !content.to_s.empty?
|
|
@@ -966,6 +1022,8 @@ module RobotLab
|
|
|
966
1022
|
# Tracks tool call names; when a consecutive or cyclic repetition exceeds
|
|
967
1023
|
# the threshold, embeds a self-correction warning in the tool result so the
|
|
968
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.
|
|
969
1027
|
def install_doom_loop_detection
|
|
970
1028
|
threshold = @config.doom_loop_threshold || DoomLoopDetector::DEFAULT_THRESHOLD
|
|
971
1029
|
detector = DoomLoopDetector.new(threshold: threshold)
|
|
@@ -989,6 +1047,7 @@ module RobotLab
|
|
|
989
1047
|
end
|
|
990
1048
|
|
|
991
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.
|
|
992
1051
|
def remove_doom_loop_detection
|
|
993
1052
|
sc = @chat.singleton_class
|
|
994
1053
|
sc.remove_method(:execute_tool) if sc.method_defined?(:execute_tool)
|
|
@@ -1004,6 +1063,9 @@ module RobotLab
|
|
|
1004
1063
|
# Fires the :compaction hook family (before/around/after_compaction).
|
|
1005
1064
|
# An on_compaction handler can replace the default strategy entirely by
|
|
1006
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.
|
|
1007
1069
|
def maybe_compact(network: nil)
|
|
1008
1070
|
return if @chat.messages.empty?
|
|
1009
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
|