robot_lab 0.1.0 → 0.2.6
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/.architecture/AGENTS.md +32 -0
- data/.architecture/config.yml +8 -0
- data/.architecture/members.yml +60 -0
- data/.architecture/reviews/feature-free-will.md +490 -0
- data/.architecture/reviews/overall-codebase.md +427 -0
- data/.claude/settings.local.json +57 -0
- data/.codex/config.toml +2 -0
- data/.envrc +1 -0
- data/.irbrc +2 -2
- data/.loki +60 -0
- data/.quality/reek_baseline.txt +43 -0
- data/.rubocop.yml +10 -0
- data/CHANGELOG.md +126 -0
- data/CLAUDE.md +139 -0
- data/README.md +130 -73
- data/Rakefile +131 -9
- data/agent2agent_review.md +192 -0
- data/agentf_improvements.md +253 -0
- data/agents.md +14 -0
- data/docs/api/core/index.md +1 -0
- data/docs/api/core/network.md +31 -1
- data/docs/api/core/robot.md +91 -5
- data/docs/api/errors.md +47 -2
- data/docs/api/index.md +3 -0
- data/docs/architecture/core-concepts.md +40 -0
- data/docs/architecture/network-orchestration.md +8 -0
- data/docs/architecture/robot-execution.md +1 -1
- data/docs/examples/index.md +37 -2
- data/docs/getting-started/configuration.md +39 -7
- data/docs/guides/building-robots.md +39 -1
- data/docs/guides/creating-networks.md +6 -1
- data/docs/guides/hooks.md +997 -0
- data/docs/guides/index.md +19 -14
- data/docs/guides/knowledge.md +9 -3
- data/docs/guides/observability.md +206 -7
- data/docs/guides/using-tools.md +69 -0
- data/docs/index.md +31 -4
- data/docs/superpowers/plans/2026-05-06-agentskills.md +1303 -0
- data/docs/superpowers/specs/2026-05-06-agentskills-design.md +247 -0
- data/examples/.envrc +1 -0
- data/examples/01_simple_robot.rb +5 -9
- data/examples/02_tools.rb +5 -9
- data/examples/03_network.rb +18 -10
- data/examples/04_mcp.rb +21 -29
- data/examples/05_streaming.rb +12 -18
- data/examples/06_prompt_templates.rb +11 -19
- data/examples/07_network_memory.rb +16 -31
- data/examples/08_llm_config.rb +10 -22
- data/examples/09_chaining.rb +16 -27
- data/examples/10_memory.rb +12 -28
- data/examples/11_network_introspection.rb +15 -29
- data/examples/12_message_bus.rb +5 -12
- data/examples/13_spawn.rb +5 -10
- data/examples/14_rusty_circuit/.envrc +1 -0
- data/examples/14_rusty_circuit/comic.rb +2 -0
- data/examples/14_rusty_circuit/heckler.rb +1 -1
- data/examples/14_rusty_circuit/open_mic.rb +1 -3
- data/examples/14_rusty_circuit/scout.rb +2 -0
- data/examples/15_memory_network_and_bus/.envrc +1 -0
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +6 -3
- data/examples/15_memory_network_and_bus/linux_writer.rb +1 -1
- data/examples/15_memory_network_and_bus/output/combined_article.md +6 -6
- data/examples/15_memory_network_and_bus/output/final_article.md +6 -8
- data/examples/15_memory_network_and_bus/output/linux_draft.md +4 -2
- data/examples/15_memory_network_and_bus/output/mac_draft.md +3 -3
- data/examples/15_memory_network_and_bus/output/memory.json +6 -6
- data/examples/15_memory_network_and_bus/output/revision_1.md +10 -11
- data/examples/15_memory_network_and_bus/output/revision_2.md +6 -8
- data/examples/15_memory_network_and_bus/output/windows_draft.md +3 -3
- data/examples/16_writers_room/.envrc +1 -0
- data/examples/16_writers_room/writers_room.rb +2 -4
- data/examples/17_skills.rb +8 -17
- data/examples/18_rails/Gemfile +1 -0
- data/examples/19_token_tracking.rb +9 -15
- data/examples/20_circuit_breaker.rb +10 -19
- data/examples/21_learning_loop.rb +11 -20
- data/examples/22_context_compression.rb +6 -13
- data/examples/23_convergence.rb +6 -17
- data/examples/24_structured_delegation.rb +11 -15
- data/examples/25_history_search.rb +5 -12
- data/examples/26_document_store.rb +6 -13
- data/examples/27_incident_response/incident_response.rb +4 -5
- data/examples/28_mcp_discovery.rb +8 -11
- data/examples/29_ractor_tools.rb +4 -9
- data/examples/30_ractor_network.rb +10 -19
- data/examples/31_launch_assessment.rb +10 -23
- data/examples/32_newsletter_reader.rb +188 -0
- data/examples/33_stock_generator.rb +80 -0
- data/examples/33_stock_predictor.rb +306 -0
- data/examples/34_agentskills.rb +72 -0
- data/examples/35_hooks.rb +256 -0
- data/examples/README.md +8 -1
- data/examples/common.rb +80 -0
- data/examples/ruboruby.md +423 -0
- data/examples/xyzzy.rb +90 -0
- data/lib/robot_lab/agent_skill.rb +64 -0
- data/lib/robot_lab/agent_skill_catalog.rb +74 -0
- data/lib/robot_lab/ask_user.rb +2 -2
- data/lib/robot_lab/budget/ledger.rb +98 -0
- data/lib/robot_lab/bus_poller.rb +12 -5
- data/lib/robot_lab/capabilities.rb +84 -0
- data/lib/robot_lab/config/defaults.yml +10 -0
- data/lib/robot_lab/config.rb +5 -16
- data/lib/robot_lab/delegation_future.rb +1 -1
- data/lib/robot_lab/doom_loop_detector.rb +98 -0
- data/lib/robot_lab/error.rb +34 -4
- data/lib/robot_lab/errors.rb +45 -0
- data/lib/robot_lab/history_compressor.rb +4 -10
- data/lib/robot_lab/hook.rb +79 -0
- data/lib/robot_lab/hook_context.rb +194 -0
- data/lib/robot_lab/hook_registry.rb +55 -0
- data/lib/robot_lab/hooks.rb +87 -0
- data/lib/robot_lab/mcp/client.rb +1 -2
- data/lib/robot_lab/mcp/connection_poller.rb +5 -5
- data/lib/robot_lab/mcp/server.rb +1 -1
- data/lib/robot_lab/mcp/server_discovery.rb +0 -2
- data/lib/robot_lab/memory.rb +32 -27
- data/lib/robot_lab/memory_change.rb +2 -2
- data/lib/robot_lab/message.rb +4 -4
- data/lib/robot_lab/narrator.rb +87 -0
- data/lib/robot_lab/network.rb +87 -25
- data/lib/robot_lab/robot/agent_skill_matching.rb +99 -0
- data/lib/robot_lab/robot/budget.rb +89 -0
- data/lib/robot_lab/robot/bus_messaging.rb +79 -33
- data/lib/robot_lab/robot/history_search.rb +4 -1
- data/lib/robot_lab/robot/hooking.rb +56 -0
- data/lib/robot_lab/robot/mcp_management.rb +12 -12
- data/lib/robot_lab/robot/template_rendering.rb +62 -42
- data/lib/robot_lab/robot.rb +482 -242
- data/lib/robot_lab/robot_result.rb +6 -5
- data/lib/robot_lab/run_config.rb +27 -14
- data/lib/robot_lab/runnable.rb +51 -0
- data/lib/robot_lab/sandbox/null.rb +13 -0
- data/lib/robot_lab/sandbox/seatbelt.rb +104 -0
- data/lib/robot_lab/sandbox.rb +52 -0
- data/lib/robot_lab/script_tool.rb +134 -0
- data/lib/robot_lab/state_proxy.rb +7 -5
- data/lib/robot_lab/task.rb +26 -20
- data/lib/robot_lab/tool.rb +54 -13
- data/lib/robot_lab/tool_config.rb +1 -1
- data/lib/robot_lab/tool_manifest.rb +5 -7
- data/lib/robot_lab/user_message.rb +2 -2
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab/waiter.rb +1 -1
- data/lib/robot_lab.rb +89 -51
- data/logfile +8 -0
- data/mkdocs.yml +3 -3
- data/robot_concurrency.md +38 -0
- data/simple_acp_review.md +298 -0
- data/temp.md +6 -0
- data/tool_manifest_plan.md +155 -0
- metadata +82 -83
- data/docs/examples/rails-application.md +0 -419
- data/docs/guides/ractor-parallelism.md +0 -364
- data/docs/guides/rails-integration.md +0 -681
- data/docs/superpowers/plans/2026-04-14-ractor-integration.md +0 -1538
- data/docs/superpowers/specs/2026-04-14-ractor-integration-design.md +0 -258
- data/lib/generators/robot_lab/install_generator.rb +0 -90
- data/lib/generators/robot_lab/job_generator.rb +0 -40
- data/lib/generators/robot_lab/robot_generator.rb +0 -55
- data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -42
- data/lib/generators/robot_lab/templates/job.rb.tt +0 -21
- data/lib/generators/robot_lab/templates/migration.rb.tt +0 -32
- data/lib/generators/robot_lab/templates/result_model.rb.tt +0 -52
- data/lib/generators/robot_lab/templates/robot.rb.tt +0 -31
- data/lib/generators/robot_lab/templates/robot_job.rb.tt +0 -18
- data/lib/generators/robot_lab/templates/robot_test.rb.tt +0 -34
- data/lib/generators/robot_lab/templates/routing_robot.rb.tt +0 -59
- data/lib/generators/robot_lab/templates/thread_model.rb.tt +0 -40
- data/lib/robot_lab/document_store.rb +0 -155
- data/lib/robot_lab/ractor_boundary.rb +0 -42
- data/lib/robot_lab/ractor_job.rb +0 -37
- data/lib/robot_lab/ractor_memory_proxy.rb +0 -85
- data/lib/robot_lab/ractor_network_scheduler.rb +0 -154
- data/lib/robot_lab/ractor_worker_pool.rb +0 -117
- data/lib/robot_lab/rails_integration/engine.rb +0 -29
- data/lib/robot_lab/rails_integration/job.rb +0 -158
- data/lib/robot_lab/rails_integration/railtie.rb +0 -51
- data/lib/robot_lab/rails_integration/turbo_stream_callbacks.rb +0 -72
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
# Classification surface for "should this error be retried?" — so hosts
|
|
5
|
+
# (ActiveJob retry_on lists, robot_lab-to's takeover loop, custom retry
|
|
6
|
+
# wrappers) don't reimplement the same case statement against RobotLab's
|
|
7
|
+
# error hierarchy.
|
|
8
|
+
#
|
|
9
|
+
# - +InferenceError+ (transient LLM/API failures) is always retryable,
|
|
10
|
+
# except its more specific subclass +ToolLoopError+ (a circuit breaker
|
|
11
|
+
# tripped by a repeating tool-call pattern — retrying immediately would
|
|
12
|
+
# just re-trigger the same loop).
|
|
13
|
+
# - +MCPError+ and +ToolError+ are opt-in: retryable only when raised
|
|
14
|
+
# with +retryable: true+, since some instances are transient (a
|
|
15
|
+
# connection drop, a timeout) and some are not (an explicit rejection
|
|
16
|
+
# from the server or tool).
|
|
17
|
+
# - Everything else (+ConfigurationError+, +ToolNotFoundError+,
|
|
18
|
+
# +DependencyError+, +RactorBoundaryError+, +BusError+, and any
|
|
19
|
+
# non-RobotLab error) is never retryable.
|
|
20
|
+
module Errors
|
|
21
|
+
# @param error [Exception, nil]
|
|
22
|
+
# @return [Boolean] true when the host should retry the operation that raised +error+
|
|
23
|
+
def self.retryable?(error)
|
|
24
|
+
return false if error.nil? || error.is_a?(RobotLab::ToolLoopError)
|
|
25
|
+
|
|
26
|
+
case error
|
|
27
|
+
when RobotLab::MCPError, RobotLab::ToolError
|
|
28
|
+
error.respond_to?(:retryable) && error.retryable == true
|
|
29
|
+
when RobotLab::InferenceError
|
|
30
|
+
true
|
|
31
|
+
else
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Always-retryable error classes, for explicit ActiveJob-style
|
|
37
|
+
# +retry_on+ allow-lists. Excludes +MCPError+/+ToolError+ because
|
|
38
|
+
# their retryability is per-raise (via +retryable:+), not per-class.
|
|
39
|
+
#
|
|
40
|
+
# @return [Array<Class>]
|
|
41
|
+
def self.retryable_classes
|
|
42
|
+
[RobotLab::InferenceError].freeze
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -124,13 +124,11 @@ module RobotLab
|
|
|
124
124
|
|
|
125
125
|
if score >= @keep_threshold
|
|
126
126
|
:keep
|
|
127
|
-
elsif score < @drop_threshold
|
|
127
|
+
elsif score < @drop_threshold || !@summarizer
|
|
128
128
|
:drop
|
|
129
|
-
|
|
129
|
+
else
|
|
130
130
|
summary = @summarizer.call(text).to_s.strip
|
|
131
131
|
summary.empty? ? :drop : summary
|
|
132
|
-
else
|
|
133
|
-
:drop
|
|
134
132
|
end
|
|
135
133
|
end
|
|
136
134
|
|
|
@@ -141,10 +139,7 @@ module RobotLab
|
|
|
141
139
|
@messages.each_with_index do |msg, idx|
|
|
142
140
|
action = actions[idx]
|
|
143
141
|
|
|
144
|
-
if action.nil?
|
|
145
|
-
# Pinned or recent: always include
|
|
146
|
-
result << msg
|
|
147
|
-
elsif action == :keep
|
|
142
|
+
if action.nil? || action == :keep
|
|
148
143
|
result << msg
|
|
149
144
|
elsif action == :drop
|
|
150
145
|
# Omit entirely
|
|
@@ -166,7 +161,7 @@ module RobotLab
|
|
|
166
161
|
role = msg.role
|
|
167
162
|
|
|
168
163
|
return true if role == :system
|
|
169
|
-
return true if
|
|
164
|
+
return true if %i[tool tool_result].include?(role)
|
|
170
165
|
|
|
171
166
|
# Assistant tool-call dispatcher: content is nil or blank
|
|
172
167
|
if role == :assistant
|
|
@@ -185,7 +180,6 @@ module RobotLab
|
|
|
185
180
|
case content
|
|
186
181
|
when String then content
|
|
187
182
|
when Array then content.filter_map { |p| p[:text] || p["text"] }.join(" ")
|
|
188
|
-
else nil
|
|
189
183
|
end
|
|
190
184
|
end
|
|
191
185
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
# Base class for hook handlers.
|
|
5
|
+
#
|
|
6
|
+
# Subclasses implement class methods for each hook they handle.
|
|
7
|
+
# Unimplemented hooks are silently skipped; around hooks passthrough
|
|
8
|
+
# automatically so the chain never breaks.
|
|
9
|
+
#
|
|
10
|
+
# The namespace defaults to the snake_case form of the last segment of the
|
|
11
|
+
# class name (e.g. TokenLogger -> :token_logger). Override with
|
|
12
|
+
# `self.namespace = :custom` when needed.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# class AuditHook < RobotLab::Hook
|
|
16
|
+
# def self.before_run(ctx)
|
|
17
|
+
# ctx.local.started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# def self.after_run(ctx)
|
|
21
|
+
# elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - ctx.local.started_at
|
|
22
|
+
# puts "[#{ctx.robot.name}] #{(elapsed * 1000).round(1)}ms"
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# def self.on_error(ctx)
|
|
26
|
+
# puts "[#{ctx.robot.name}] ERROR: #{ctx.error.message}"
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# RobotLab.on(AuditHook)
|
|
31
|
+
# robot.on(AuditHook)
|
|
32
|
+
# network.on(AuditHook)
|
|
33
|
+
#
|
|
34
|
+
class Hook
|
|
35
|
+
@namespace = nil
|
|
36
|
+
|
|
37
|
+
class << self
|
|
38
|
+
attr_writer :namespace
|
|
39
|
+
|
|
40
|
+
# Returns the hook's namespace symbol, derived from the class name by
|
|
41
|
+
# default. Returns nil for the base Hook class itself.
|
|
42
|
+
#
|
|
43
|
+
# @return [Symbol, nil]
|
|
44
|
+
def namespace
|
|
45
|
+
return @namespace if @namespace
|
|
46
|
+
return nil if self == Hook
|
|
47
|
+
|
|
48
|
+
name.split('::').last
|
|
49
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
50
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
51
|
+
.downcase
|
|
52
|
+
.to_sym
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Dispatch a single hook to this handler.
|
|
56
|
+
#
|
|
57
|
+
# If the handler implements the method it is called. For around hooks that
|
|
58
|
+
# are not implemented the block is called directly (passthrough). Non-around
|
|
59
|
+
# hooks that are not implemented are silent no-ops.
|
|
60
|
+
#
|
|
61
|
+
# @param hook_name [Symbol] e.g. :before_run, :around_tool_call
|
|
62
|
+
# @param context [HookContext] the hook context object
|
|
63
|
+
# @yield for around hooks — the next link in the chain
|
|
64
|
+
def call(hook_name, context, &block)
|
|
65
|
+
if singleton_class.public_method_defined?(hook_name)
|
|
66
|
+
block ? public_send(hook_name, context, &block)
|
|
67
|
+
: public_send(hook_name, context)
|
|
68
|
+
elsif block
|
|
69
|
+
block.call
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def inherited(subclass)
|
|
74
|
+
super
|
|
75
|
+
subclass.instance_variable_set(:@namespace, nil)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
class HookContext
|
|
5
|
+
attr_reader :event, :metadata
|
|
6
|
+
|
|
7
|
+
def initialize(event:, metadata: nil)
|
|
8
|
+
@event = event.to_sym
|
|
9
|
+
@metadata = metadata || ExtensionState.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def ext(name)
|
|
13
|
+
metadata.ext(name)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def local
|
|
17
|
+
raise ArgumentError, "No hook namespace active" unless @namespace
|
|
18
|
+
|
|
19
|
+
ext(@namespace)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def with_namespace(namespace)
|
|
23
|
+
previous = @namespace
|
|
24
|
+
@namespace = namespace&.to_sym
|
|
25
|
+
yield self
|
|
26
|
+
ensure
|
|
27
|
+
@namespace = previous
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_h
|
|
31
|
+
public_snapshot.merge(metadata: metadata.to_h)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def public_snapshot
|
|
37
|
+
public_methods(false)
|
|
38
|
+
.grep_v(/=\z/)
|
|
39
|
+
.reject { |name| %i[to_h local].include?(name) }
|
|
40
|
+
.sort
|
|
41
|
+
.to_h { |name| [name, public_send(name)] }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class RunHookContext < HookContext
|
|
46
|
+
attr_reader :robot, :network, :task, :memory, :config
|
|
47
|
+
attr_accessor :request, :response, :error
|
|
48
|
+
|
|
49
|
+
def initialize(robot:, request:, network: nil, task: nil, memory: nil, config: nil, response: nil, error: nil, **)
|
|
50
|
+
super(event: :run, **)
|
|
51
|
+
@robot = robot
|
|
52
|
+
@network = network
|
|
53
|
+
@task = task
|
|
54
|
+
@memory = memory
|
|
55
|
+
@config = config
|
|
56
|
+
@request = request
|
|
57
|
+
@response = response
|
|
58
|
+
@error = error
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
class LlmGenerationHookContext < RunHookContext
|
|
63
|
+
attr_reader :iteration
|
|
64
|
+
attr_accessor :generation_response
|
|
65
|
+
|
|
66
|
+
def initialize(iteration: 0, generation_response: nil, **)
|
|
67
|
+
super(**)
|
|
68
|
+
@iteration = iteration
|
|
69
|
+
@generation_response = generation_response
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class ToolCallHookContext < HookContext
|
|
74
|
+
attr_reader :tool, :tool_name, :tool_args, :robot
|
|
75
|
+
attr_accessor :tool_result, :tool_error
|
|
76
|
+
|
|
77
|
+
def initialize(tool:, tool_args:, robot: nil, tool_result: nil, tool_error: nil, **)
|
|
78
|
+
super(event: :tool_call, **)
|
|
79
|
+
@tool = tool
|
|
80
|
+
@tool_name = tool.respond_to?(:name) ? tool.name : tool.class.name
|
|
81
|
+
@tool_args = tool_args
|
|
82
|
+
@robot = robot
|
|
83
|
+
@tool_result = tool_result
|
|
84
|
+
@tool_error = tool_error
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
class NetworkRunHookContext < HookContext
|
|
89
|
+
attr_reader :network, :memory, :config
|
|
90
|
+
attr_accessor :context, :result, :error
|
|
91
|
+
|
|
92
|
+
def initialize(network:, context:, memory: nil, config: nil, result: nil, error: nil, **)
|
|
93
|
+
super(event: :network_run, **)
|
|
94
|
+
@network = network
|
|
95
|
+
@context = context
|
|
96
|
+
@memory = memory
|
|
97
|
+
@config = config
|
|
98
|
+
@result = result
|
|
99
|
+
@error = error
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
class TaskHookContext < HookContext
|
|
104
|
+
attr_reader :network, :task, :task_name, :robot, :memory, :config
|
|
105
|
+
attr_accessor :result, :error
|
|
106
|
+
|
|
107
|
+
def initialize(task:, network: nil, robot: nil, memory: nil, config: nil, result: nil, error: nil, **)
|
|
108
|
+
super(event: :task, **)
|
|
109
|
+
@network = network
|
|
110
|
+
@task = task
|
|
111
|
+
@task_name = task.name
|
|
112
|
+
@robot = robot || task.robot
|
|
113
|
+
@memory = memory
|
|
114
|
+
@config = config
|
|
115
|
+
@result = result
|
|
116
|
+
@error = error
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
class LearnHookContext < HookContext
|
|
121
|
+
attr_reader :robot, :text, :learnings_before
|
|
122
|
+
attr_accessor :stored, :error
|
|
123
|
+
|
|
124
|
+
def initialize(robot:, text:, learnings_before:, **)
|
|
125
|
+
super(event: :learn, **)
|
|
126
|
+
@robot = robot
|
|
127
|
+
@text = text
|
|
128
|
+
@learnings_before = learnings_before.freeze
|
|
129
|
+
@stored = false
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
class CompactionHookContext < HookContext
|
|
134
|
+
attr_reader :robot, :messages_before, :config, :strategy
|
|
135
|
+
attr_accessor :compacted_messages, :error
|
|
136
|
+
|
|
137
|
+
def initialize(robot:, messages_before:, config:, strategy:, **)
|
|
138
|
+
super(event: :compaction, **)
|
|
139
|
+
@robot = robot
|
|
140
|
+
@messages_before = messages_before.freeze
|
|
141
|
+
@config = config
|
|
142
|
+
@strategy = strategy
|
|
143
|
+
@compacted_messages = nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# True once an on_compaction handler has supplied a replacement message set.
|
|
147
|
+
def handled?
|
|
148
|
+
!@compacted_messages.nil?
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
class ExtensionState
|
|
153
|
+
def initialize
|
|
154
|
+
@states = Hash.new { |h, key| h[key] = DotState.new }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def ext(name)
|
|
158
|
+
@states[name.to_sym]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def to_h
|
|
162
|
+
@states.transform_values(&:to_h)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
class DotState
|
|
167
|
+
def initialize
|
|
168
|
+
@data = {}
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def merge_defaults(hash)
|
|
172
|
+
hash.each { |k, v| @data[k.to_sym] = v unless @data.key?(k.to_sym) }
|
|
173
|
+
self
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def method_missing(name, *args)
|
|
177
|
+
key = name.to_s.delete_suffix('=').to_sym
|
|
178
|
+
|
|
179
|
+
if name.to_s.end_with?('=')
|
|
180
|
+
@data[key] = args.first
|
|
181
|
+
else
|
|
182
|
+
@data[key]
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def respond_to_missing?(*)
|
|
187
|
+
true
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def to_h
|
|
191
|
+
@data.dup
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
class HookRegistry
|
|
5
|
+
Registration = Data.define(:handler_class, :context) do
|
|
6
|
+
def initialize(handler_class:, context: nil)
|
|
7
|
+
super
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def namespace
|
|
11
|
+
handler_class.namespace
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@registrations = []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Register a hook handler class.
|
|
20
|
+
#
|
|
21
|
+
# @param handler_class [Class] a RobotLab::Hook subclass
|
|
22
|
+
# @param context [Hash, nil] optional default values merged into ctx.local on each call
|
|
23
|
+
# @return [Registration]
|
|
24
|
+
def on(handler_class, context: nil)
|
|
25
|
+
unless handler_class.is_a?(Class) && handler_class < RobotLab::Hook
|
|
26
|
+
raise ArgumentError, "#{handler_class.inspect} must be a RobotLab::Hook subclass"
|
|
27
|
+
end
|
|
28
|
+
raise ArgumentError, "#{handler_class} has no namespace" if handler_class.namespace.nil?
|
|
29
|
+
|
|
30
|
+
registration = Registration.new(handler_class: handler_class, context: context)
|
|
31
|
+
@registrations << registration
|
|
32
|
+
registration
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Returns registrations whose handler implements the given hook method.
|
|
36
|
+
#
|
|
37
|
+
# @param hook_name [Symbol]
|
|
38
|
+
# @return [Array<Registration>]
|
|
39
|
+
def registrations_for(hook_name)
|
|
40
|
+
hook_sym = hook_name.to_sym
|
|
41
|
+
@registrations.select { |reg| reg.handler_class.singleton_class.public_method_defined?(hook_sym) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Returns all registrations (defensive copy).
|
|
45
|
+
#
|
|
46
|
+
# @return [Array<Registration>]
|
|
47
|
+
def registrations
|
|
48
|
+
@registrations.dup
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def clear
|
|
52
|
+
@registrations.clear
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
module Hooks
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def run(family, context, registries:, per_run_hooks: nil, &)
|
|
8
|
+
before = registrations(:"before_#{family}", registries, per_run_hooks)
|
|
9
|
+
around = registrations(:"around_#{family}", registries, per_run_hooks)
|
|
10
|
+
after = registrations(:"after_#{family}", registries, per_run_hooks)
|
|
11
|
+
errors = error_registrations(family, registries, per_run_hooks)
|
|
12
|
+
|
|
13
|
+
call_all(before, :"before_#{family}", context)
|
|
14
|
+
result = call_around(around, :"around_#{family}", context, &)
|
|
15
|
+
set_result(context, family, result)
|
|
16
|
+
call_all(after, :"after_#{family}", context)
|
|
17
|
+
result
|
|
18
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
19
|
+
context.error = e if context.respond_to?(:error=)
|
|
20
|
+
call_all(errors, :on_error, context)
|
|
21
|
+
raise
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call(hook_name, context, registries:, per_run_hooks: nil)
|
|
25
|
+
call_all(registrations(hook_name, registries, per_run_hooks), hook_name, context)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def around(hook_name, context, registries:, per_run_hooks: nil, &)
|
|
29
|
+
call_around(registrations(hook_name, registries, per_run_hooks), hook_name, context, &)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def registrations(hook_name, registries, per_run_hooks)
|
|
33
|
+
registry_entries = registries.compact.flat_map { |registry| registry.registrations_for(hook_name) }
|
|
34
|
+
registry_entries + per_run_entries(hook_name, per_run_hooks)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def error_registrations(family, registries, per_run_hooks)
|
|
38
|
+
return [] unless %i[run network_run task].include?(family.to_sym)
|
|
39
|
+
|
|
40
|
+
registrations(:on_error, registries, per_run_hooks)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def call_all(registrations, hook_name, context)
|
|
44
|
+
registrations.each { |registration| call_registration(registration, hook_name, context) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def call_around(registrations, hook_name, context, &block)
|
|
48
|
+
chain = registrations.reverse.inject(block) do |next_link, registration|
|
|
49
|
+
proc { call_registration(registration, hook_name, context, &next_link) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
chain.call
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def call_registration(registration, hook_name, context, &)
|
|
56
|
+
context.with_namespace(registration.namespace) do
|
|
57
|
+
if registration.context && registration.namespace
|
|
58
|
+
context.ext(registration.namespace).merge_defaults(registration.context)
|
|
59
|
+
end
|
|
60
|
+
registration.handler_class.call(hook_name, context, &)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# per_run_hooks is now a single handler class or an array of handler classes.
|
|
65
|
+
def per_run_entries(hook_name, hooks)
|
|
66
|
+
return [] unless hooks
|
|
67
|
+
|
|
68
|
+
hook_sym = hook_name.to_sym
|
|
69
|
+
Array(hooks).filter_map do |handler_class|
|
|
70
|
+
next unless handler_class.singleton_class.public_method_defined?(hook_sym)
|
|
71
|
+
|
|
72
|
+
HookRegistry::Registration.new(handler_class: handler_class)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def set_result(context, family, result)
|
|
77
|
+
case family.to_sym
|
|
78
|
+
when :run
|
|
79
|
+
context.response = result if context.respond_to?(:response=)
|
|
80
|
+
when :network_run, :task
|
|
81
|
+
context.result = result if context.respond_to?(:result=)
|
|
82
|
+
when :llm_generation
|
|
83
|
+
context.generation_response = result if context.respond_to?(:generation_response=)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
data/lib/robot_lab/mcp/client.rb
CHANGED
|
@@ -141,8 +141,7 @@ module RobotLab
|
|
|
141
141
|
#
|
|
142
142
|
def get_prompt(name, arguments = {})
|
|
143
143
|
ensure_connected!
|
|
144
|
-
|
|
145
|
-
response
|
|
144
|
+
request(method: "prompts/get", params: { name: name, arguments: arguments })
|
|
146
145
|
end
|
|
147
146
|
|
|
148
147
|
# Checks if the client is connected to the server.
|
|
@@ -115,7 +115,7 @@ module RobotLab
|
|
|
115
115
|
client.transport.stdin.flush
|
|
116
116
|
rescue Errno::EPIPE, IOError => e
|
|
117
117
|
@mutex.synchronize { @clients[io][:queue] = nil }
|
|
118
|
-
raise MCPError
|
|
118
|
+
raise MCPError.new("MCP connection lost: #{e.message}", retryable: true)
|
|
119
119
|
end
|
|
120
120
|
|
|
121
121
|
response = Timeout.timeout(timeout) { queue.pop }
|
|
@@ -127,7 +127,7 @@ module RobotLab
|
|
|
127
127
|
response
|
|
128
128
|
rescue Timeout::Error
|
|
129
129
|
@mutex.synchronize { @clients[io]&.[]= :queue, nil }
|
|
130
|
-
raise MCPError
|
|
130
|
+
raise MCPError.new("MCP server did not respond within #{timeout}s", retryable: true)
|
|
131
131
|
ensure
|
|
132
132
|
@mutex.synchronize { @clients[io]&.[]= :queue, nil }
|
|
133
133
|
end
|
|
@@ -145,15 +145,15 @@ module RobotLab
|
|
|
145
145
|
loop do
|
|
146
146
|
ios = @mutex.synchronize { @clients.keys.reject(&:closed?) }
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
if ios.empty?
|
|
149
|
+
sleep POLL_INTERVAL
|
|
150
|
+
else
|
|
149
151
|
begin
|
|
150
152
|
ready, = IO.select(ios, nil, nil, POLL_INTERVAL)
|
|
151
153
|
dispatch(ready) if ready
|
|
152
154
|
rescue Errno::EBADF
|
|
153
155
|
# A pipe was closed between the reject and IO.select — harmless, loop again
|
|
154
156
|
end
|
|
155
|
-
else
|
|
156
|
-
sleep POLL_INTERVAL
|
|
157
157
|
end
|
|
158
158
|
|
|
159
159
|
break unless @mutex.synchronize { @running }
|
data/lib/robot_lab/mcp/server.rb
CHANGED