robot_lab 0.1.0 → 0.2.1
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/.irbrc +2 -2
- data/.rubocop.yml +172 -0
- data/CHANGELOG.md +72 -0
- data/CLAUDE.md +139 -0
- data/README.md +91 -95
- data/Rakefile +109 -3
- data/agent2agent_review.md +192 -0
- data/agentf_improvements.md +253 -0
- data/agents.md +14 -0
- data/docs/examples/index.md +37 -2
- data/docs/getting-started/configuration.md +20 -7
- data/docs/guides/index.md +16 -16
- data/docs/guides/knowledge.md +7 -1
- data/docs/guides/observability.md +132 -0
- data/docs/index.md +30 -3
- 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 +8 -9
- 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/README.md +1 -1
- data/examples/common.rb +76 -0
- data/examples/ruboruby.md +423 -0
- data/examples/temp.md +51 -0
- data/lib/robot_lab/agent_skill.rb +63 -0
- data/lib/robot_lab/agent_skill_catalog.rb +74 -0
- data/lib/robot_lab/ask_user.rb +2 -2
- data/lib/robot_lab/bus_poller.rb +12 -5
- data/lib/robot_lab/config.rb +1 -12
- data/lib/robot_lab/delegation_future.rb +1 -1
- data/lib/robot_lab/doom_loop_detector.rb +98 -0
- data/lib/robot_lab/history_compressor.rb +4 -10
- data/lib/robot_lab/mcp/client.rb +1 -2
- data/lib/robot_lab/mcp/connection_poller.rb +3 -3
- 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/network.rb +11 -6
- data/lib/robot_lab/robot/agent_skill_matching.rb +99 -0
- data/lib/robot_lab/robot/bus_messaging.rb +9 -27
- data/lib/robot_lab/robot/history_search.rb +4 -1
- data/lib/robot_lab/robot/mcp_management.rb +5 -11
- data/lib/robot_lab/robot/template_rendering.rb +60 -40
- data/lib/robot_lab/robot.rb +323 -206
- data/lib/robot_lab/robot_result.rb +6 -5
- data/lib/robot_lab/run_config.rb +5 -11
- data/lib/robot_lab/script_tool.rb +76 -0
- data/lib/robot_lab/state_proxy.rb +7 -5
- data/lib/robot_lab/tool.rb +3 -3
- 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 +41 -52
- data/logfile +8 -0
- data/mkdocs.yml +2 -3
- data/robot_concurrency.md +38 -0
- data/simple_acp_review.md +298 -0
- data/site/404.html +2300 -0
- data/site/api/core/index.html +2706 -0
- data/site/api/core/memory/index.html +3793 -0
- data/site/api/core/network/index.html +3500 -0
- data/site/api/core/robot/index.html +4566 -0
- data/site/api/core/state/index.html +3390 -0
- data/site/api/core/tool/index.html +3843 -0
- data/site/api/index.html +2635 -0
- data/site/api/mcp/client/index.html +3435 -0
- data/site/api/mcp/index.html +2783 -0
- data/site/api/mcp/server/index.html +3252 -0
- data/site/api/mcp/transports/index.html +3352 -0
- data/site/api/messages/index.html +2641 -0
- data/site/api/messages/text-message/index.html +3087 -0
- data/site/api/messages/tool-call-message/index.html +3159 -0
- data/site/api/messages/tool-result-message/index.html +3252 -0
- data/site/api/messages/user-message/index.html +3212 -0
- data/site/api/streaming/context/index.html +3282 -0
- data/site/api/streaming/events/index.html +3347 -0
- data/site/api/streaming/index.html +2738 -0
- data/site/architecture/core-concepts/index.html +3757 -0
- data/site/architecture/index.html +2797 -0
- data/site/architecture/message-flow/index.html +3238 -0
- data/site/architecture/network-orchestration/index.html +3433 -0
- data/site/architecture/robot-execution/index.html +3140 -0
- data/site/architecture/state-management/index.html +3498 -0
- data/site/assets/css/custom.css +56 -0
- data/site/assets/images/favicon.png +0 -0
- data/site/assets/images/robot_lab.jpg +0 -0
- data/site/assets/javascripts/bundle.79ae519e.min.js +16 -0
- data/site/assets/javascripts/bundle.79ae519e.min.js.map +7 -0
- data/site/assets/javascripts/lunr/min/lunr.ar.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.da.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.de.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.du.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.el.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.es.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.fi.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.fr.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.he.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.hi.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.hu.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.hy.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.it.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.ja.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.jp.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.kn.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.ko.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.multi.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.nl.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.no.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.pt.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.ro.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.ru.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.sa.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.sv.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.ta.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.te.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.th.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.tr.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.vi.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.zh.min.js +1 -0
- data/site/assets/javascripts/lunr/tinyseg.js +206 -0
- data/site/assets/javascripts/lunr/wordcut.js +6708 -0
- data/site/assets/javascripts/workers/search.2c215733.min.js +42 -0
- data/site/assets/javascripts/workers/search.2c215733.min.js.map +7 -0
- data/site/assets/stylesheets/main.484c7ddc.min.css +1 -0
- data/site/assets/stylesheets/main.484c7ddc.min.css.map +1 -0
- data/site/assets/stylesheets/palette.ab4e12ef.min.css +1 -0
- data/site/assets/stylesheets/palette.ab4e12ef.min.css.map +1 -0
- data/site/concepts/index.html +3455 -0
- data/site/examples/basic-chat/index.html +2880 -0
- data/site/examples/index.html +2907 -0
- data/site/examples/mcp-server/index.html +3018 -0
- data/site/examples/multi-robot-network/index.html +3131 -0
- data/site/examples/rails-application/index.html +3329 -0
- data/site/examples/tool-usage/index.html +3085 -0
- data/site/getting-started/configuration/index.html +3745 -0
- data/site/getting-started/index.html +2572 -0
- data/site/getting-started/installation/index.html +2981 -0
- data/site/getting-started/quick-start/index.html +2942 -0
- data/site/guides/building-robots/index.html +4290 -0
- data/site/guides/creating-networks/index.html +3858 -0
- data/site/guides/index.html +2586 -0
- data/site/guides/mcp-integration/index.html +3581 -0
- data/site/guides/memory/index.html +3586 -0
- data/site/guides/rails-integration/index.html +4019 -0
- data/site/guides/streaming/index.html +3157 -0
- data/site/guides/using-tools/index.html +3802 -0
- data/site/index.html +2671 -0
- data/site/search/search_index.json +1 -0
- data/site/sitemap.xml +183 -0
- data/site/sitemap.xml.gz +0 -0
- data/site/tags.json +1 -0
- data/temp.md +6 -0
- data/tool_manifest_plan.md +155 -0
- metadata +154 -92
- 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,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
class Robot < RubyLLM::Agent
|
|
5
|
+
# Prepended module that intercepts run() to inject relevant AgentSkills.io
|
|
6
|
+
# skills into the system prompt and tool list for the duration of each call.
|
|
7
|
+
#
|
|
8
|
+
# Owns: @_active_agent_skills, @_agent_skill_original_instructions, @_agent_skill_injected_tools
|
|
9
|
+
# Reads: @pending_agent_skills, @agent_skill_store, @local_tools, @chat, @name
|
|
10
|
+
# Contract: prepended (wraps super); requires TemplateRendering to have set @pending_agent_skills
|
|
11
|
+
#
|
|
12
|
+
# Skills are matched by embedding similarity between the incoming message
|
|
13
|
+
# and each pending skill's description (via DocumentStore/fastembed).
|
|
14
|
+
# Injected content is fully restored in an ensure block after run() returns.
|
|
15
|
+
module AgentSkillMatching
|
|
16
|
+
SIMILARITY_THRESHOLD = 0.70
|
|
17
|
+
|
|
18
|
+
def run(message = nil, **, &)
|
|
19
|
+
matched = match_agent_skills(message.to_s)
|
|
20
|
+
inject_agent_skills(matched) if matched.any?
|
|
21
|
+
super
|
|
22
|
+
ensure
|
|
23
|
+
restore_after_agent_skills if @_active_agent_skills&.any?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Override to re-inject skill instructions after template re-render replaces
|
|
27
|
+
# the system prompt during a run() call with runtime kwargs.
|
|
28
|
+
def rerender_template(run_context)
|
|
29
|
+
super
|
|
30
|
+
return unless @_active_agent_skills&.any?
|
|
31
|
+
|
|
32
|
+
@_agent_skill_original_instructions = current_agent_skill_instructions
|
|
33
|
+
prepend_skill_instructions(@_active_agent_skills)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# Find pending AgentSkills whose descriptions are semantically similar to message.
|
|
39
|
+
#
|
|
40
|
+
# @param message [String]
|
|
41
|
+
# @param threshold [Float] cosine similarity cutoff (default SIMILARITY_THRESHOLD)
|
|
42
|
+
# @return [Array<AgentSkill>]
|
|
43
|
+
def match_agent_skills(message, threshold: SIMILARITY_THRESHOLD)
|
|
44
|
+
return [] if @pending_agent_skills.nil? || @pending_agent_skills.empty?
|
|
45
|
+
|
|
46
|
+
results = @agent_skill_store.search(message, limit: @pending_agent_skills.size)
|
|
47
|
+
results
|
|
48
|
+
.select { |r| r[:score] >= threshold }
|
|
49
|
+
.filter_map { |r| @pending_agent_skills.find { |s| s.name.to_sym == r[:key] } }
|
|
50
|
+
rescue => e
|
|
51
|
+
RobotLab.config.logger.warn(
|
|
52
|
+
"Robot '#{@name}': AgentSkill embedding failed: #{e.message}"
|
|
53
|
+
)
|
|
54
|
+
[]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Prepend skill instructions before existing system prompt content.
|
|
58
|
+
#
|
|
59
|
+
# @param skills [Array<AgentSkill>]
|
|
60
|
+
def prepend_skill_instructions(skills)
|
|
61
|
+
skill_content = skills.map(&:instructions).join("\n\n")
|
|
62
|
+
base = @_agent_skill_original_instructions.to_s
|
|
63
|
+
combined = [skill_content, base].reject(&:empty?).join("\n\n")
|
|
64
|
+
@chat.with_instructions(combined)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Inject script tools and snapshot instructions before injection.
|
|
68
|
+
#
|
|
69
|
+
# @param skills [Array<AgentSkill>]
|
|
70
|
+
def inject_agent_skills(skills)
|
|
71
|
+
return if skills.empty?
|
|
72
|
+
|
|
73
|
+
@_active_agent_skills = skills
|
|
74
|
+
@_agent_skill_original_instructions = current_agent_skill_instructions
|
|
75
|
+
prepend_skill_instructions(skills)
|
|
76
|
+
@_agent_skill_injected_tools = skills.flat_map(&:script_tools).compact
|
|
77
|
+
@local_tools += @_agent_skill_injected_tools
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Remove injected tools and restore original system prompt.
|
|
81
|
+
def restore_after_agent_skills
|
|
82
|
+
@local_tools -= @_agent_skill_injected_tools || []
|
|
83
|
+
@chat.with_instructions(@_agent_skill_original_instructions.to_s)
|
|
84
|
+
@_active_agent_skills = nil
|
|
85
|
+
@_agent_skill_original_instructions = nil
|
|
86
|
+
@_agent_skill_injected_tools = nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Read the current system message content from the underlying chat.
|
|
90
|
+
#
|
|
91
|
+
# @return [String, nil]
|
|
92
|
+
def current_agent_skill_instructions
|
|
93
|
+
messages = @chat.instance_variable_get(:@messages)
|
|
94
|
+
sys = messages&.find { |m| m.role.to_s == "system" }
|
|
95
|
+
sys&.content
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -16,6 +16,9 @@ module RobotLab
|
|
|
16
16
|
# inline. The BusPoller drains each group's queue sequentially on
|
|
17
17
|
# a dedicated OS thread, so robot.run() calls never interleave.
|
|
18
18
|
#
|
|
19
|
+
# Owns: @bus, @bus_poller, @private_bus_poller, @bus_poller_group, @bus_subscriber_id, @message_counter, @outbox, @message_handler
|
|
20
|
+
# Reads: @name
|
|
21
|
+
# Contract: ivars initialized by initialize_runtime_state before first bus operation
|
|
19
22
|
module BusMessaging
|
|
20
23
|
# Send a message to another robot via the bus.
|
|
21
24
|
#
|
|
@@ -33,7 +36,6 @@ module RobotLab
|
|
|
33
36
|
message
|
|
34
37
|
end
|
|
35
38
|
|
|
36
|
-
|
|
37
39
|
# Send a reply to a specific message via the bus.
|
|
38
40
|
#
|
|
39
41
|
# @param to [String, Symbol] target robot's channel name
|
|
@@ -50,7 +52,6 @@ module RobotLab
|
|
|
50
52
|
reply
|
|
51
53
|
end
|
|
52
54
|
|
|
53
|
-
|
|
54
55
|
# Register a custom handler for incoming bus messages.
|
|
55
56
|
#
|
|
56
57
|
# Block arity controls delivery handling:
|
|
@@ -64,7 +65,6 @@ module RobotLab
|
|
|
64
65
|
self
|
|
65
66
|
end
|
|
66
67
|
|
|
67
|
-
|
|
68
68
|
# Spawn a new robot on a shared bus.
|
|
69
69
|
#
|
|
70
70
|
# Creates a new Robot instance that shares this robot's bus,
|
|
@@ -79,7 +79,7 @@ module RobotLab
|
|
|
79
79
|
# @param options [Hash] additional options passed to RobotLab.build
|
|
80
80
|
# @return [Robot] the newly created robot
|
|
81
81
|
#
|
|
82
|
-
def spawn(name: "robot", system_prompt: nil, template: nil, local_tools: [], **
|
|
82
|
+
def spawn(name: "robot", system_prompt: nil, template: nil, local_tools: [], **)
|
|
83
83
|
ensure_bus
|
|
84
84
|
|
|
85
85
|
RobotLab.build(
|
|
@@ -88,11 +88,10 @@ module RobotLab
|
|
|
88
88
|
template: template,
|
|
89
89
|
local_tools: local_tools,
|
|
90
90
|
bus: @bus,
|
|
91
|
-
**
|
|
91
|
+
**
|
|
92
92
|
)
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
-
|
|
96
95
|
# Connect this robot to a message bus.
|
|
97
96
|
#
|
|
98
97
|
# If a bus is provided, the robot joins it. If no bus is provided
|
|
@@ -134,7 +133,6 @@ module RobotLab
|
|
|
134
133
|
with_bus unless @bus
|
|
135
134
|
end
|
|
136
135
|
|
|
137
|
-
|
|
138
136
|
# Create a typed channel on the bus and subscribe to it.
|
|
139
137
|
# Auto-creates a private BusPoller if none has been assigned.
|
|
140
138
|
def setup_bus_channel
|
|
@@ -149,7 +147,6 @@ module RobotLab
|
|
|
149
147
|
@bus_subscriber_id = @bus.subscribe(channel_name) { |delivery| enqueue_delivery(delivery) }
|
|
150
148
|
end
|
|
151
149
|
|
|
152
|
-
|
|
153
150
|
# Unsubscribe from the bus channel and stop the private poller if any.
|
|
154
151
|
def teardown_bus_channel
|
|
155
152
|
channel_name = @name.to_sym
|
|
@@ -162,13 +159,11 @@ module RobotLab
|
|
|
162
159
|
@bus_poller_group = :default
|
|
163
160
|
end
|
|
164
161
|
|
|
165
|
-
|
|
166
162
|
# Enqueue a delivery to the robot's assigned poller.
|
|
167
163
|
def enqueue_delivery(delivery)
|
|
168
164
|
@bus_poller.enqueue(robot: self, delivery: delivery, group: @bus_poller_group)
|
|
169
165
|
end
|
|
170
166
|
|
|
171
|
-
|
|
172
167
|
# Process a single delivery (called by BusPoller drain thread).
|
|
173
168
|
def process_delivery(delivery)
|
|
174
169
|
message = delivery.message
|
|
@@ -180,30 +175,17 @@ module RobotLab
|
|
|
180
175
|
entry[:replies] << message
|
|
181
176
|
end
|
|
182
177
|
|
|
183
|
-
if @message_handler
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
@message_handler.call(message)
|
|
187
|
-
else
|
|
188
|
-
@message_handler.call(delivery, message)
|
|
189
|
-
end
|
|
178
|
+
if @message_handler.arity == 1
|
|
179
|
+
delivery.ack!
|
|
180
|
+
@message_handler.call(message)
|
|
190
181
|
else
|
|
191
|
-
|
|
182
|
+
@message_handler.call(delivery, message)
|
|
192
183
|
end
|
|
193
184
|
rescue => e
|
|
194
185
|
delivery.nack! if delivery.pending?
|
|
195
186
|
raise BusError, "Error handling bus message on robot '#{@name}': #{e.message}"
|
|
196
187
|
end
|
|
197
188
|
|
|
198
|
-
|
|
199
|
-
# Default handler: interpret message via LLM and reply
|
|
200
|
-
def handle_message_via_llm(delivery, message)
|
|
201
|
-
delivery.ack!
|
|
202
|
-
result = run(message.content.to_s)
|
|
203
|
-
send_reply(to: message.from.to_sym, content: result.last_text_content, in_reply_to: message.key)
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
|
|
207
189
|
# Publish a RobotMessage to a bus channel
|
|
208
190
|
def publish_to_bus(channel_name, message)
|
|
209
191
|
if defined?(Async::Task) && Async::Task.current?
|
|
@@ -4,6 +4,10 @@ module RobotLab
|
|
|
4
4
|
class Robot
|
|
5
5
|
# Semantic search over a robot's conversation history.
|
|
6
6
|
#
|
|
7
|
+
# Owns: nothing (read-only mixin)
|
|
8
|
+
# Reads: @chat (specifically @chat.messages)
|
|
9
|
+
# Contract: no initialization required; safe to call any time after initialize
|
|
10
|
+
#
|
|
7
11
|
# Scores each message in @chat.messages against the query using stemmed
|
|
8
12
|
# term-frequency cosine similarity (via the +classifier+ gem). Returns the
|
|
9
13
|
# top-N messages ranked by relevance.
|
|
@@ -61,7 +65,6 @@ module RobotLab
|
|
|
61
65
|
case content
|
|
62
66
|
when String then content
|
|
63
67
|
when Array then content.filter_map { |p| p[:text] || p["text"] }.join(" ")
|
|
64
|
-
else nil
|
|
65
68
|
end
|
|
66
69
|
end
|
|
67
70
|
end
|
|
@@ -4,9 +4,9 @@ module RobotLab
|
|
|
4
4
|
class Robot < RubyLLM::Agent
|
|
5
5
|
# MCP client lifecycle and hierarchical tool/MCP resolution.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
# @mcp_config, @tools_config, @
|
|
9
|
-
#
|
|
7
|
+
# Owns: @mcp_clients, @mcp_tools, @mcp_initialized, @failed_mcp_configs
|
|
8
|
+
# Reads: @mcp_config, @tools_config, @name, @chat, @local_tools
|
|
9
|
+
# Contract: ivars initialized by initialize_runtime_state; lazy init on first run
|
|
10
10
|
module MCPManagement
|
|
11
11
|
private
|
|
12
12
|
|
|
@@ -17,7 +17,6 @@ module RobotLab
|
|
|
17
17
|
ToolConfig.resolve_mcp(runtime_value, parent_value: build_resolved)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
|
|
21
20
|
# Resolve tools hierarchy: runtime -> robot build -> network -> config
|
|
22
21
|
def resolve_tools_hierarchy(runtime_value, network: nil, network_config: nil)
|
|
23
22
|
parent_value = network_config&.tools || network&.network&.tools || RobotLab.config.tools
|
|
@@ -25,7 +24,6 @@ module RobotLab
|
|
|
25
24
|
ToolConfig.resolve_tools(runtime_value, parent_value: build_resolved)
|
|
26
25
|
end
|
|
27
26
|
|
|
28
|
-
|
|
29
27
|
# Ensure MCP clients are initialized for the given server configs.
|
|
30
28
|
# On subsequent calls, retries any servers that previously failed to connect.
|
|
31
29
|
def ensure_mcp_clients(mcp_servers)
|
|
@@ -50,7 +48,6 @@ module RobotLab
|
|
|
50
48
|
@mcp_initialized = true
|
|
51
49
|
end
|
|
52
50
|
|
|
53
|
-
|
|
54
51
|
def init_mcp_client(server_config)
|
|
55
52
|
client = MCP::Client.new(server_config)
|
|
56
53
|
client.connect
|
|
@@ -74,13 +71,12 @@ module RobotLab
|
|
|
74
71
|
)
|
|
75
72
|
end
|
|
76
73
|
|
|
77
|
-
|
|
78
74
|
# Retry connecting to servers that previously failed
|
|
79
|
-
def retry_failed_servers(
|
|
75
|
+
def retry_failed_servers(_mcp_servers, needed_servers)
|
|
80
76
|
return if @failed_mcp_configs.nil? || @failed_mcp_configs.empty?
|
|
81
77
|
|
|
82
78
|
# Only retry servers that are still needed and still failed
|
|
83
|
-
to_retry = @failed_mcp_configs.
|
|
79
|
+
to_retry = @failed_mcp_configs.slice(*needed_servers)
|
|
84
80
|
return if to_retry.empty?
|
|
85
81
|
|
|
86
82
|
to_retry.each do |name, server_config|
|
|
@@ -106,7 +102,6 @@ module RobotLab
|
|
|
106
102
|
end
|
|
107
103
|
end
|
|
108
104
|
|
|
109
|
-
|
|
110
105
|
def discover_mcp_tools(client, server_name)
|
|
111
106
|
tools = client.list_tools
|
|
112
107
|
|
|
@@ -129,7 +124,6 @@ module RobotLab
|
|
|
129
124
|
)
|
|
130
125
|
end
|
|
131
126
|
|
|
132
|
-
|
|
133
127
|
def extract_server_name(server_config)
|
|
134
128
|
case server_config
|
|
135
129
|
when Hash
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'set'
|
|
4
|
-
|
|
5
3
|
module RobotLab
|
|
6
4
|
class Robot < RubyLLM::Agent
|
|
7
5
|
# Template loading, rendering, and front-matter extraction.
|
|
8
6
|
#
|
|
9
|
-
#
|
|
10
|
-
# @chat, @template, @build_context, @name, @name_from_constructor,
|
|
11
|
-
#
|
|
7
|
+
# Owns: @expanded_skills, @pending_agent_skills, @agent_skill_store
|
|
8
|
+
# Reads: @chat, @template, @build_context, @name, @name_from_constructor, @description, @local_tools, @mcp_config, @config
|
|
9
|
+
# Contract: called during initialize after assign_identity_ivars and build_effective_config
|
|
12
10
|
module TemplateRendering
|
|
13
11
|
# Front matter keys that map to chat configuration methods
|
|
14
12
|
FRONT_MATTER_CONFIG_KEYS = %i[
|
|
@@ -64,7 +62,6 @@ module RobotLab
|
|
|
64
62
|
end
|
|
65
63
|
end
|
|
66
64
|
|
|
67
|
-
|
|
68
65
|
# Re-render the template with run-time context merged into build-time context.
|
|
69
66
|
# prompt_manager parameters may be required (null) and only available at run time.
|
|
70
67
|
def rerender_template(run_context)
|
|
@@ -93,64 +90,85 @@ module RobotLab
|
|
|
93
90
|
end
|
|
94
91
|
end
|
|
95
92
|
|
|
96
|
-
|
|
97
93
|
# Orchestrate skill expansion and template application.
|
|
98
94
|
#
|
|
99
95
|
# @param skill_ids [Array<Symbol>] skill IDs from constructor + front matter
|
|
100
96
|
# @param context [Hash, Proc] variables to pass to all templates
|
|
101
97
|
def apply_skills_and_template_to_chat(skill_ids, context)
|
|
98
|
+
bodies, accumulated_config, extras = collect_prompt_content(skill_ids, context)
|
|
99
|
+
apply_prompt_to_chat(bodies, accumulated_config, extras)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Expand skills and render all bodies, configs, and extras into plain data.
|
|
103
|
+
# Pure computation — reads ivars but does not mutate @chat.
|
|
104
|
+
#
|
|
105
|
+
# @return [Array(Array<String>, RunConfig, Hash)] bodies, merged config, extras hash
|
|
106
|
+
def collect_prompt_content(skill_ids, context)
|
|
102
107
|
visited = Set.new
|
|
103
|
-
# Prevent skills from pulling in the main template
|
|
104
108
|
visited.add(@template) if @template
|
|
105
|
-
|
|
106
109
|
@expanded_skills = expand_skills(skill_ids, visited)
|
|
107
110
|
|
|
108
111
|
extras = {}
|
|
109
112
|
accumulated_config = RunConfig.new
|
|
110
113
|
bodies = []
|
|
111
|
-
|
|
112
114
|
resolved_ctx = resolve_context(context, network: nil)
|
|
113
115
|
|
|
114
|
-
# Process each expanded skill
|
|
115
116
|
@expanded_skills.each do |skill_id|
|
|
116
117
|
parsed = PM.parse(skill_id)
|
|
117
118
|
accumulate_extras(parsed.metadata, extras)
|
|
118
|
-
|
|
119
|
-
accumulated_config = accumulated_config.merge(fm_config)
|
|
119
|
+
accumulated_config = accumulated_config.merge(RunConfig.from_front_matter(parsed.metadata))
|
|
120
120
|
body = render_body(parsed, resolved_ctx)
|
|
121
121
|
bodies << body if body
|
|
122
122
|
end
|
|
123
123
|
|
|
124
|
-
# Process main template if present
|
|
125
124
|
if @template
|
|
126
125
|
parsed = PM.parse(@template)
|
|
127
126
|
accumulate_extras(parsed.metadata, extras)
|
|
128
|
-
|
|
129
|
-
accumulated_config = accumulated_config.merge(fm_config)
|
|
127
|
+
accumulated_config = accumulated_config.merge(RunConfig.from_front_matter(parsed.metadata))
|
|
130
128
|
body = render_body(parsed, resolved_ctx)
|
|
131
129
|
bodies << body if body
|
|
132
130
|
end
|
|
133
131
|
|
|
134
|
-
|
|
132
|
+
[bodies, accumulated_config, extras]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Apply collected prompt content to @chat.
|
|
136
|
+
# Pure mutation — takes plain data and writes to @chat.
|
|
137
|
+
#
|
|
138
|
+
# @param bodies [Array<String>] rendered template bodies
|
|
139
|
+
# @param accumulated_config [RunConfig] merged skill + template config
|
|
140
|
+
# @param extras [Hash] accumulated front-matter extras (name, description, tools, mcp)
|
|
141
|
+
def apply_prompt_to_chat(bodies, accumulated_config, extras)
|
|
135
142
|
apply_accumulated_extras(extras)
|
|
136
143
|
|
|
137
|
-
# Constructor config overrides skill + template config
|
|
138
144
|
effective = accumulated_config.merge(@config)
|
|
139
145
|
effective.apply_to(@chat)
|
|
140
146
|
|
|
141
|
-
# Set instructions once with all bodies joined
|
|
142
147
|
combined = bodies.join("\n\n")
|
|
143
148
|
@chat.with_instructions(combined) unless combined.empty?
|
|
144
149
|
end
|
|
145
150
|
|
|
146
|
-
|
|
147
151
|
# Recursively expand skill IDs depth-first.
|
|
152
|
+
# Checks AgentSkillCatalog first; falls back to PM template lookup.
|
|
148
153
|
# Returns a flat Array<Symbol> in processing order (deepest first).
|
|
149
154
|
#
|
|
150
155
|
# @param skill_ids [Array<Symbol>] skill IDs to expand
|
|
151
156
|
# @param visited [Set<Symbol>] already-visited IDs for cycle detection
|
|
152
157
|
# @return [Array<Symbol>] flat ordered list of skill IDs
|
|
153
158
|
def expand_skills(skill_ids, visited = Set.new)
|
|
159
|
+
expand_skills_with_catalog(skill_ids, visited, AgentSkillCatalog.instance)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Recursively expand skill IDs depth-first, using the given catalog.
|
|
163
|
+
# AgentSkills folder format takes priority over PM template lookup.
|
|
164
|
+
# Catalog hits are stored in @pending_agent_skills / @agent_skill_store
|
|
165
|
+
# and are NOT included in the returned Array (they are handled separately).
|
|
166
|
+
#
|
|
167
|
+
# @param skill_ids [Array<Symbol>] skill IDs to expand
|
|
168
|
+
# @param visited [Set<Symbol>] already-visited IDs for cycle detection
|
|
169
|
+
# @param catalog [AgentSkillCatalog] catalog to check first
|
|
170
|
+
# @return [Array<Symbol>] flat ordered list of PM-based skill IDs
|
|
171
|
+
def expand_skills_with_catalog(skill_ids, visited, catalog)
|
|
154
172
|
result = []
|
|
155
173
|
|
|
156
174
|
skill_ids.each do |skill_id|
|
|
@@ -165,21 +183,32 @@ module RobotLab
|
|
|
165
183
|
|
|
166
184
|
visited.add(skill_id)
|
|
167
185
|
|
|
168
|
-
#
|
|
186
|
+
# Check catalog first: AgentSkills folder format takes priority
|
|
187
|
+
if (agent_skill = catalog.find(skill_id))
|
|
188
|
+
@pending_agent_skills ||= []
|
|
189
|
+
unless defined?(RobotLab::DocumentStore)
|
|
190
|
+
raise LoadError,
|
|
191
|
+
"robot_lab-document_store is required to use AgentSkill catalogs. " \
|
|
192
|
+
"Add `gem 'robot_lab-document_store'` to your Gemfile."
|
|
193
|
+
end
|
|
194
|
+
@agent_skill_store ||= RobotLab::DocumentStore.new
|
|
195
|
+
@pending_agent_skills << agent_skill
|
|
196
|
+
@agent_skill_store.store(agent_skill.name.to_sym, agent_skill.description)
|
|
197
|
+
next
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Fall back to PM template (existing behavior)
|
|
169
201
|
parsed = PM.parse(skill_id)
|
|
170
202
|
nested = extract_skills_from_metadata(parsed.metadata)
|
|
171
203
|
|
|
172
|
-
|
|
173
|
-
result.concat(expand_skills(nested, visited)) if nested.any?
|
|
204
|
+
result.concat(expand_skills_with_catalog(nested, visited, catalog)) if nested.any?
|
|
174
205
|
|
|
175
|
-
# Then append this skill
|
|
176
206
|
result << skill_id
|
|
177
207
|
end
|
|
178
208
|
|
|
179
209
|
result
|
|
180
210
|
end
|
|
181
211
|
|
|
182
|
-
|
|
183
212
|
# Extract skills array from metadata.
|
|
184
213
|
#
|
|
185
214
|
# @param metadata [PM::Metadata] front matter metadata
|
|
@@ -190,7 +219,6 @@ module RobotLab
|
|
|
190
219
|
Array(metadata.skills).map(&:to_sym)
|
|
191
220
|
end
|
|
192
221
|
|
|
193
|
-
|
|
194
222
|
# Accumulate extras from metadata into a hash.
|
|
195
223
|
# Later calls overwrite earlier values (last-write-wins).
|
|
196
224
|
#
|
|
@@ -209,12 +237,10 @@ module RobotLab
|
|
|
209
237
|
extras[:tools] = metadata.tools
|
|
210
238
|
end
|
|
211
239
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
end
|
|
240
|
+
return unless metadata.respond_to?(:mcp) && metadata.mcp.is_a?(Array)
|
|
241
|
+
extras[:mcp] = metadata.mcp.map { |m| m.is_a?(Hash) ? m.transform_keys(&:to_sym) : m }
|
|
215
242
|
end
|
|
216
243
|
|
|
217
|
-
|
|
218
244
|
# Apply accumulated extras to the robot, respecting constructor precedence.
|
|
219
245
|
#
|
|
220
246
|
# @param extras [Hash] accumulated extras from skills + main template
|
|
@@ -231,12 +257,10 @@ module RobotLab
|
|
|
231
257
|
@local_tools = resolve_frontmatter_tools(extras[:tools])
|
|
232
258
|
end
|
|
233
259
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
end
|
|
260
|
+
return unless extras[:mcp] && ToolConfig.none_value?(@mcp_config)
|
|
261
|
+
@mcp_config = extras[:mcp]
|
|
237
262
|
end
|
|
238
263
|
|
|
239
|
-
|
|
240
264
|
# Extract identity and capability keys from front matter metadata.
|
|
241
265
|
# Constructor-provided values take precedence over frontmatter.
|
|
242
266
|
def apply_front_matter_extras(metadata)
|
|
@@ -252,12 +276,10 @@ module RobotLab
|
|
|
252
276
|
@local_tools = resolve_frontmatter_tools(metadata.tools)
|
|
253
277
|
end
|
|
254
278
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
end
|
|
279
|
+
return unless metadata.respond_to?(:mcp) && metadata.mcp.is_a?(Array) && ToolConfig.none_value?(@mcp_config)
|
|
280
|
+
@mcp_config = metadata.mcp.map { |m| m.is_a?(Hash) ? m.transform_keys(&:to_sym) : m }
|
|
258
281
|
end
|
|
259
282
|
|
|
260
|
-
|
|
261
283
|
# Render a parsed template body, returning nil if required params are missing.
|
|
262
284
|
#
|
|
263
285
|
# @param parsed [PM::Parsed] the parsed template
|
|
@@ -271,7 +293,6 @@ module RobotLab
|
|
|
271
293
|
nil
|
|
272
294
|
end
|
|
273
295
|
|
|
274
|
-
|
|
275
296
|
# Resolve string tool names from frontmatter to Ruby constants.
|
|
276
297
|
# Tool subclasses are instantiated; instances are used as-is.
|
|
277
298
|
# Unresolvable names are skipped with a warning.
|
|
@@ -294,7 +315,6 @@ module RobotLab
|
|
|
294
315
|
end
|
|
295
316
|
end
|
|
296
317
|
|
|
297
|
-
|
|
298
318
|
def resolve_context(context, network:)
|
|
299
319
|
case context
|
|
300
320
|
when Proc then context.call(network: network)
|