robot_lab 0.3.0 → 0.5.0
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/CHANGELOG.md +60 -0
- data/README.md +29 -19
- data/lib/robot_lab/errors.rb +4 -0
- data/lib/robot_lab/hook_context.rb +9 -2
- data/lib/robot_lab/mcp/registry.rb +99 -0
- data/lib/robot_lab/mcp/server.rb +47 -83
- data/lib/robot_lab/mcp/server_discovery.rb +22 -9
- data/lib/robot_lab/mcp.rb +259 -0
- data/lib/robot_lab/robot/mcp_management.rb +159 -105
- data/lib/robot_lab/robot/tool_call_gating.rb +144 -0
- data/lib/robot_lab/robot.rb +72 -99
- data/lib/robot_lab/run_config.rb +3 -2
- data/lib/robot_lab/tool.rb +8 -23
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +1 -4
- metadata +16 -292
- data/.architecture/AGENTS.md +0 -32
- data/.architecture/config.yml +0 -8
- data/.architecture/members.yml +0 -60
- data/.architecture/reviews/feature-free-will.md +0 -490
- data/.architecture/reviews/overall-codebase.md +0 -427
- data/.claude/settings.local.json +0 -57
- data/.codex/config.toml +0 -2
- data/.envrc +0 -6
- data/.github/workflows/deploy-github-pages.yml +0 -52
- data/.irbrc +0 -6
- data/.loki +0 -65
- data/.quality/reek_baseline.txt +0 -43
- data/.rubocop.yml +0 -10
- data/Archspec.rb +0 -44
- data/CLAUDE.md +0 -141
- data/COMMITS.md +0 -196
- data/Rakefile +0 -193
- data/_typos.toml +0 -21
- data/agent2agent_review.md +0 -192
- data/agentf_improvements.md +0 -253
- data/agents.md +0 -14
- data/docs/api/core/index.md +0 -147
- data/docs/api/core/memory.md +0 -578
- data/docs/api/core/network.md +0 -553
- data/docs/api/core/result.md +0 -211
- data/docs/api/core/robot.md +0 -1667
- data/docs/api/core/state.md +0 -145
- data/docs/api/core/tool.md +0 -520
- data/docs/api/errors.md +0 -323
- data/docs/api/hooks.md +0 -469
- data/docs/api/index.md +0 -137
- data/docs/api/mcp/client.md +0 -387
- data/docs/api/mcp/index.md +0 -267
- data/docs/api/mcp/server.md +0 -252
- data/docs/api/mcp/transports.md +0 -339
- data/docs/api/messages/index.md +0 -126
- data/docs/api/messages/text-message.md +0 -170
- data/docs/api/messages/tool-call-message.md +0 -206
- data/docs/api/messages/tool-result-message.md +0 -242
- data/docs/api/messages/user-message.md +0 -232
- data/docs/api/skills.md +0 -423
- data/docs/api/streaming/context.md +0 -280
- data/docs/api/streaming/events.md +0 -185
- data/docs/api/streaming/index.md +0 -156
- data/docs/api/support.md +0 -846
- data/docs/architecture/core-concepts.md +0 -575
- data/docs/architecture/index.md +0 -252
- data/docs/architecture/message-flow.md +0 -366
- data/docs/architecture/network-orchestration.md +0 -478
- data/docs/architecture/robot-execution.md +0 -396
- data/docs/architecture/state-management.md +0 -410
- data/docs/assets/css/custom.css +0 -56
- data/docs/assets/images/robot_lab.jpg +0 -0
- data/docs/concepts.md +0 -641
- data/docs/examples/basic-chat.md +0 -258
- data/docs/examples/index.md +0 -425
- data/docs/examples/mcp-server.md +0 -403
- data/docs/examples/multi-robot-network.md +0 -476
- data/docs/examples/tool-usage.md +0 -468
- data/docs/getting-started/configuration.md +0 -645
- data/docs/getting-started/index.md +0 -56
- data/docs/getting-started/installation.md +0 -197
- data/docs/getting-started/quick-start.md +0 -293
- data/docs/guides/building-robots.md +0 -1187
- data/docs/guides/creating-networks.md +0 -736
- data/docs/guides/hooks.md +0 -1206
- data/docs/guides/index.md +0 -78
- data/docs/guides/knowledge.md +0 -219
- data/docs/guides/mcp-integration.md +0 -740
- data/docs/guides/memory.md +0 -458
- data/docs/guides/observability.md +0 -733
- data/docs/guides/streaming.md +0 -323
- data/docs/guides/using-tools.md +0 -780
- data/docs/index.md +0 -280
- data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
- data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
- data/examples/.envrc +0 -3
- data/examples/01_simple_robot.rb +0 -31
- data/examples/02_tools.rb +0 -109
- data/examples/03_network.rb +0 -118
- data/examples/04_mcp.rb +0 -217
- data/examples/05_streaming.rb +0 -137
- data/examples/06_prompt_templates.rb +0 -325
- data/examples/07_network_memory.rb +0 -313
- data/examples/08_llm_config.rb +0 -162
- data/examples/09_chaining.rb +0 -251
- data/examples/10_memory.rb +0 -315
- data/examples/11_network_introspection.rb +0 -239
- data/examples/12_message_bus.rb +0 -67
- data/examples/13_spawn.rb +0 -85
- data/examples/14_rusty_circuit/.envrc +0 -1
- data/examples/14_rusty_circuit/.gitignore +0 -1
- data/examples/14_rusty_circuit/README.md +0 -1
- data/examples/14_rusty_circuit/comic.rb +0 -149
- data/examples/14_rusty_circuit/display.rb +0 -217
- data/examples/14_rusty_circuit/heckler.rb +0 -65
- data/examples/14_rusty_circuit/open_mic.rb +0 -132
- data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
- data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
- data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
- data/examples/14_rusty_circuit/scout.rb +0 -165
- data/examples/14_rusty_circuit/show.log +0 -234
- data/examples/15_memory_network_and_bus/.envrc +0 -1
- data/examples/15_memory_network_and_bus/README.md +0 -66
- data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
- data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
- data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
- data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
- data/examples/15_memory_network_and_bus/output/combined_article.md +0 -11
- data/examples/15_memory_network_and_bus/output/final_article.md +0 -8
- data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
- data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
- data/examples/15_memory_network_and_bus/output/memory.json +0 -13
- data/examples/15_memory_network_and_bus/output/revision_1.md +0 -29
- data/examples/15_memory_network_and_bus/output/revision_2.md +0 -50
- data/examples/15_memory_network_and_bus/output/revision_3.md +0 -8
- data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
- data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
- data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
- data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
- data/examples/16_writers_room/.envrc +0 -1
- data/examples/16_writers_room/display.rb +0 -158
- data/examples/16_writers_room/output/.gitignore +0 -4
- data/examples/16_writers_room/output/README.md +0 -69
- data/examples/16_writers_room/output/opus_001.md +0 -263
- data/examples/16_writers_room/output/opus_001_notes.log +0 -470
- data/examples/16_writers_room/output/opus_002.md +0 -245
- data/examples/16_writers_room/output/opus_002_notes.log +0 -546
- data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
- data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
- data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
- data/examples/16_writers_room/prompts/writer.md +0 -37
- data/examples/16_writers_room/room.rb +0 -186
- data/examples/16_writers_room/tools.rb +0 -173
- data/examples/16_writers_room/writer.rb +0 -121
- data/examples/16_writers_room/writers_room.rb +0 -256
- data/examples/17_skills.rb +0 -234
- data/examples/18_rails/.envrc +0 -3
- data/examples/18_rails/.gitignore +0 -5
- data/examples/18_rails/Gemfile +0 -11
- data/examples/18_rails/README.md +0 -67
- data/examples/18_rails/Rakefile +0 -4
- data/examples/18_rails/app/controllers/application_controller.rb +0 -4
- data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
- data/examples/18_rails/app/jobs/application_job.rb +0 -4
- data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
- data/examples/18_rails/app/models/application_record.rb +0 -5
- data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
- data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
- data/examples/18_rails/app/robots/chat_robot.rb +0 -25
- data/examples/18_rails/app/tools/time_tool.rb +0 -9
- data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
- data/examples/18_rails/app/views/chat/index.html.erb +0 -67
- data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
- data/examples/18_rails/bin/dev +0 -7
- data/examples/18_rails/bin/rails +0 -6
- data/examples/18_rails/bin/setup +0 -15
- data/examples/18_rails/config/application.rb +0 -33
- data/examples/18_rails/config/cable.yml +0 -2
- data/examples/18_rails/config/database.yml +0 -5
- data/examples/18_rails/config/environment.rb +0 -4
- data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
- data/examples/18_rails/config/routes.rb +0 -6
- data/examples/18_rails/config.ru +0 -4
- data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
- data/examples/19_token_tracking.rb +0 -138
- data/examples/20_circuit_breaker.rb +0 -147
- data/examples/21_learning_loop.rb +0 -181
- data/examples/22_context_compression.rb +0 -172
- data/examples/23_convergence.rb +0 -133
- data/examples/24_structured_delegation.rb +0 -151
- data/examples/25_history_search/conversation.jsonl +0 -30
- data/examples/25_history_search.rb +0 -133
- data/examples/26_document_store/api_versioning_adr.md +0 -52
- data/examples/26_document_store/incident_postmortem.md +0 -46
- data/examples/26_document_store/postgres_runbook.md +0 -49
- data/examples/26_document_store/redis_caching_guide.md +0 -48
- data/examples/26_document_store/sidekiq_guide.md +0 -51
- data/examples/26_document_store.rb +0 -140
- data/examples/27_incident_response/README.md +0 -65
- data/examples/27_incident_response/incident_response.rb +0 -261
- data/examples/28_mcp_discovery.rb +0 -113
- data/examples/29_ractor_tools.rb +0 -240
- data/examples/30_ractor_network.rb +0 -252
- data/examples/31_launch_assessment.rb +0 -246
- data/examples/32_newsletter_reader.rb +0 -193
- data/examples/33_stock_generator.rb +0 -80
- data/examples/33_stock_predictor.rb +0 -327
- data/examples/34_agentskills.rb +0 -76
- data/examples/35_hooks.rb +0 -266
- data/examples/README.md +0 -550
- data/examples/common.rb +0 -180
- data/examples/prompts/assistant.md +0 -5
- data/examples/prompts/audit_trail.md +0 -8
- data/examples/prompts/billing.md +0 -10
- data/examples/prompts/classifier.md +0 -7
- data/examples/prompts/comedian.md +0 -6
- data/examples/prompts/comedy_critic.md +0 -10
- data/examples/prompts/configurable.md +0 -9
- data/examples/prompts/dispatcher.md +0 -12
- data/examples/prompts/entity_extractor.md +0 -14
- data/examples/prompts/escalation.md +0 -42
- data/examples/prompts/frontmatter_mcp_test.md +0 -9
- data/examples/prompts/frontmatter_named_test.md +0 -5
- data/examples/prompts/frontmatter_tools_test.md +0 -6
- data/examples/prompts/general.md +0 -7
- data/examples/prompts/github_assistant.md +0 -14
- data/examples/prompts/helper.md +0 -4
- data/examples/prompts/incident_responder.md +0 -18
- data/examples/prompts/keyword_extractor.md +0 -11
- data/examples/prompts/llm_config_demo.md +0 -20
- data/examples/prompts/order_support.md +0 -35
- data/examples/prompts/os_advocate.md +0 -13
- data/examples/prompts/os_chief.md +0 -13
- data/examples/prompts/os_editor.md +0 -13
- data/examples/prompts/parameterized_main_test.md +0 -6
- data/examples/prompts/pii_redactor.md +0 -7
- data/examples/prompts/product_support.md +0 -37
- data/examples/prompts/runbook_protocol.md +0 -12
- data/examples/prompts/sentiment_analyzer.md +0 -12
- data/examples/prompts/skill_a_test.md +0 -4
- data/examples/prompts/skill_b_test.md +0 -4
- data/examples/prompts/skill_config_test.md +0 -5
- data/examples/prompts/skill_cycle_a_test.md +0 -6
- data/examples/prompts/skill_cycle_b_test.md +0 -6
- data/examples/prompts/skill_description_test.md +0 -4
- data/examples/prompts/skill_leaf_test.md +0 -4
- data/examples/prompts/skill_nested_test.md +0 -6
- data/examples/prompts/skill_refs_main_test.md +0 -6
- data/examples/prompts/skill_self_ref_test.md +0 -6
- data/examples/prompts/skill_with_mcp_test.md +0 -9
- data/examples/prompts/skill_with_params_test.md +0 -6
- data/examples/prompts/skill_with_robot_name_test.md +0 -5
- data/examples/prompts/skill_with_tools_test.md +0 -6
- data/examples/prompts/sre_compliance.md +0 -10
- data/examples/prompts/structured_output.md +0 -7
- data/examples/prompts/synthesizer.md +0 -17
- data/examples/prompts/technical.md +0 -10
- data/examples/prompts/template_with_skills_test.md +0 -6
- data/examples/prompts/triage.md +0 -22
- data/examples/ruboruby.md +0 -423
- data/examples/run_all.rb +0 -60
- data/examples/xyzzy.rb +0 -97
- data/lib/robot_lab/mcp/client.rb +0 -223
- data/lib/robot_lab/mcp/connection_poller.rb +0 -195
- data/lib/robot_lab/mcp/transports/base.rb +0 -64
- data/lib/robot_lab/mcp/transports/sse.rb +0 -120
- data/lib/robot_lab/mcp/transports/stdio.rb +0 -170
- data/lib/robot_lab/mcp/transports/streamable_http.rb +0 -141
- data/lib/robot_lab/mcp/transports/websocket.rb +0 -111
- data/logfile +0 -8
- data/mkdocs.yml +0 -209
- data/robot_concurrency.md +0 -38
- data/simple_acp_review.md +0 -298
- data/temp.md +0 -6
- data/tool_manifest_plan.md +0 -155
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
class Robot < RubyLLM::Agent
|
|
5
|
+
# What happens between the model asking for a tool and the tool answering
|
|
6
|
+
# when the tool is not a RobotLab::Tool: MCP tools and plain
|
|
7
|
+
# RubyLLM::Tool instances.
|
|
8
|
+
#
|
|
9
|
+
# - The hook bridge runs the robot's +:tool_call+ hooks around the chat's
|
|
10
|
+
# execution of such tools (RobotLab::Tool runs them itself in #call),
|
|
11
|
+
# and turns an MCP transport failure mid-call into an error result so
|
|
12
|
+
# the model can recover instead of the run aborting.
|
|
13
|
+
# - The settlement loop answers what ruby_llm pauses a run for: tool
|
|
14
|
+
# calls that require approval, MCP input requests (elicitation), and
|
|
15
|
+
# long-running MCP tasks. A run never returns half-finished.
|
|
16
|
+
#
|
|
17
|
+
# Owns: nothing (uses @chat, @tool_approval, @on_tool_approval, @on_input_request)
|
|
18
|
+
# Reads: @name
|
|
19
|
+
module ToolCallGating
|
|
20
|
+
# How long a run waits for long-running MCP tasks before giving up.
|
|
21
|
+
MCP_TASK_TIMEOUT = 300
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# Wrap the chat's execute_tool so foreign tools get :tool_call hooks.
|
|
26
|
+
# Prepended on the chat's singleton class, the bridge sits ahead of the
|
|
27
|
+
# per-run doom-loop singleton method (install_doom_loop_detection) and
|
|
28
|
+
# calls through to it with +super+.
|
|
29
|
+
def install_tool_hook_bridge
|
|
30
|
+
robot = self
|
|
31
|
+
bridge = Module.new do
|
|
32
|
+
define_method(:execute_tool) do |tool_call|
|
|
33
|
+
robot.send(:bridge_tool_call, tool_call) { super(tool_call) }
|
|
34
|
+
end
|
|
35
|
+
private :execute_tool
|
|
36
|
+
end
|
|
37
|
+
@chat.singleton_class.prepend(bridge)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Run :tool_call hooks around a chat-executed tool call. Yields to the
|
|
41
|
+
# chat's own execution; the PAUSED sentinel it returns for approvals,
|
|
42
|
+
# input requests and tasks passes through untouched.
|
|
43
|
+
# :reek:TooManyStatements -- context build, hook wrap and the MCP rescue are one guarded dispatch.
|
|
44
|
+
def bridge_tool_call(tool_call)
|
|
45
|
+
tool = @chat.tools[tool_call.name.to_sym]
|
|
46
|
+
return yield if tool.nil? || tool.is_a?(RobotLab::Tool)
|
|
47
|
+
|
|
48
|
+
context = ToolCallHookContext.new(tool: tool, tool_args: tool_call.arguments, robot: self,
|
|
49
|
+
mcp_server: mcp_server_for(tool))
|
|
50
|
+
scope = RobotLab.current_hook_scope || {}
|
|
51
|
+
RobotLab::Hooks.run(:tool_call, context,
|
|
52
|
+
registries: scope[:registries] || hook_registries,
|
|
53
|
+
per_run_hooks: scope[:per_run_hooks]) do
|
|
54
|
+
context.tool_result = yield
|
|
55
|
+
rescue RubyLLM::MCP::Error => e
|
|
56
|
+
context.tool_error = e
|
|
57
|
+
context.tool_result = { error: "#{tool.name}: #{e.message}" }
|
|
58
|
+
end
|
|
59
|
+
context.tool_result
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Keep completing the chat until nothing waits on the outside world:
|
|
63
|
+
# decide approvals, answer input requests, wait on tasks, complete again.
|
|
64
|
+
#
|
|
65
|
+
# @param response [RubyLLM::Message] what ask returned
|
|
66
|
+
# @return [RubyLLM::Message] the final response
|
|
67
|
+
def settle_pending_tool_calls(response, &)
|
|
68
|
+
deadline = nil
|
|
69
|
+
while @chat.waiting?
|
|
70
|
+
decide_pending_approvals
|
|
71
|
+
answer_pending_inputs
|
|
72
|
+
deadline = wait_for_mcp_tasks(deadline)
|
|
73
|
+
response = @chat.complete(&)
|
|
74
|
+
end
|
|
75
|
+
response
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def decide_pending_approvals
|
|
79
|
+
@chat.pending_approvals.each do |tool_call|
|
|
80
|
+
approve_tool_call?(tool_call) ? @chat.approve(tool_call) : @chat.deny(tool_call)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Approval policy: an on_tool_approval Proc decides; else tool_approval
|
|
85
|
+
# :approve allows; else deny with a warning naming the two settings.
|
|
86
|
+
#
|
|
87
|
+
# @param tool_call [RubyLLM::ToolCall]
|
|
88
|
+
# @return [Boolean]
|
|
89
|
+
def approve_tool_call?(tool_call)
|
|
90
|
+
return @on_tool_approval.call(tool_call) ? true : false if @on_tool_approval
|
|
91
|
+
return true if @tool_approval == :approve
|
|
92
|
+
|
|
93
|
+
RobotLab.config.logger.warn(
|
|
94
|
+
"Robot '#{@name}' denied tool call '#{tool_call.name}': it requires approval. " \
|
|
95
|
+
'Pass tool_approval: :approve or on_tool_approval: to allow it.'
|
|
96
|
+
)
|
|
97
|
+
false
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def answer_pending_inputs
|
|
101
|
+
@chat.pending_inputs.each { |request| settle_input_request(request) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# An on_input_request Proc answers a request with a Hash of field
|
|
105
|
+
# values (or +true+ to accept a URL request); anything else declines.
|
|
106
|
+
#
|
|
107
|
+
# @param request [RubyLLM::MCP::InputRequest]
|
|
108
|
+
def settle_input_request(request)
|
|
109
|
+
answer = @on_input_request&.call(request)
|
|
110
|
+
case answer
|
|
111
|
+
when Hash then @chat.answer(request, **answer.transform_keys(&:to_sym))
|
|
112
|
+
when true then @chat.answer(request)
|
|
113
|
+
else
|
|
114
|
+
RobotLab.config.logger.warn(
|
|
115
|
+
"Robot '#{@name}' declined an MCP input request (#{request.message}); set on_input_request: to answer it."
|
|
116
|
+
)
|
|
117
|
+
@chat.decline(request)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Sleep one poll interval when tool calls wait on MCP tasks, tracking a
|
|
122
|
+
# deadline across calls so a stuck task cannot hang the run.
|
|
123
|
+
#
|
|
124
|
+
# @param deadline [Float, nil] monotonic time to give up at, once a task has been seen
|
|
125
|
+
# @return [Float, nil] the deadline to carry forward
|
|
126
|
+
def wait_for_mcp_tasks(deadline)
|
|
127
|
+
tasks = @chat.pending_tasks
|
|
128
|
+
return nil if tasks.empty?
|
|
129
|
+
|
|
130
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
131
|
+
deadline ||= now + MCP_TASK_TIMEOUT
|
|
132
|
+
raise_mcp_task_timeout!(tasks) if now > deadline
|
|
133
|
+
|
|
134
|
+
sleep(tasks.filter_map(&:poll_interval).min || 1)
|
|
135
|
+
deadline
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def raise_mcp_task_timeout!(tasks)
|
|
139
|
+
names = tasks.map { |task| task.tool_call&.name }.compact.join(', ')
|
|
140
|
+
raise MCPError.new("MCP task(s) for #{names} did not finish within #{MCP_TASK_TIMEOUT}s", retryable: true)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
data/lib/robot_lab/robot.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'robot/template_rendering'
|
|
4
4
|
require_relative 'robot/mcp_management'
|
|
5
|
+
require_relative 'robot/tool_call_gating'
|
|
5
6
|
require_relative 'robot/bus_messaging'
|
|
6
7
|
require_relative 'robot/history_search'
|
|
7
8
|
require_relative 'robot/agent_skill_matching'
|
|
@@ -51,6 +52,7 @@ module RobotLab
|
|
|
51
52
|
class Robot < RubyLLM::Agent
|
|
52
53
|
include Robot::TemplateRendering
|
|
53
54
|
include Robot::MCPManagement
|
|
55
|
+
include Robot::ToolCallGating
|
|
54
56
|
include Robot::BusMessaging
|
|
55
57
|
include Robot::HistorySearch
|
|
56
58
|
include Robot::Budget
|
|
@@ -74,10 +76,6 @@ module RobotLab
|
|
|
74
76
|
# @return [String, nil] inline system prompt (used alone or appended to template)
|
|
75
77
|
# @!attribute [r] local_tools
|
|
76
78
|
# @return [Array] the locally defined tools for this robot
|
|
77
|
-
# @!attribute [r] mcp_clients
|
|
78
|
-
# @return [Hash<String, MCP::Client>] connected MCP clients by server name
|
|
79
|
-
# @!attribute [r] mcp_tools
|
|
80
|
-
# @return [Array<Tool>] tools discovered from MCP servers
|
|
81
79
|
# @!attribute [r] memory
|
|
82
80
|
# @return [Memory] the robot's inherent memory (used when standalone, not in network)
|
|
83
81
|
# @!attribute [rw] input
|
|
@@ -87,7 +85,7 @@ module RobotLab
|
|
|
87
85
|
attr_accessor :input, :output
|
|
88
86
|
|
|
89
87
|
attr_reader :name, :description, :template, :system_prompt,
|
|
90
|
-
:local_tools, :
|
|
88
|
+
:local_tools, :memory,
|
|
91
89
|
:bus, :outbox, :config, :skills, :provider, :hooks,
|
|
92
90
|
:total_input_tokens, :total_output_tokens, :learnings,
|
|
93
91
|
:budget_ledger
|
|
@@ -133,7 +131,8 @@ module RobotLab
|
|
|
133
131
|
auto_compact: @config.auto_compact,
|
|
134
132
|
compact_threshold: @config.compact_threshold,
|
|
135
133
|
token_budget: @config.token_budget,
|
|
136
|
-
cost_budget: @config.cost_budget
|
|
134
|
+
cost_budget: @config.cost_budget,
|
|
135
|
+
tool_approval: @config.tool_approval
|
|
137
136
|
}.compact
|
|
138
137
|
end
|
|
139
138
|
|
|
@@ -146,12 +145,19 @@ module RobotLab
|
|
|
146
145
|
# @param description [String, nil] optional description
|
|
147
146
|
# @param local_tools [Array] tools defined locally
|
|
148
147
|
# @param model [String, nil] the LLM model to use
|
|
149
|
-
# @param
|
|
150
|
-
#
|
|
148
|
+
# @param mcp [Symbol, Array] hierarchical MCP config: :none, :inherit, or an Array of
|
|
149
|
+
# config Hashes, RubyLLM::MCP instances and RubyLLM::MCP subclasses (see RobotLab::MCP)
|
|
151
150
|
# @param tools [Symbol, Array] hierarchical tools config
|
|
152
151
|
# @param on_tool_call [Proc, nil] callback invoked when a tool is called
|
|
153
152
|
# @param on_tool_result [Proc, nil] callback invoked when a tool returns a result
|
|
154
153
|
# @param on_content [Proc, nil] callback invoked with each streaming content chunk
|
|
154
|
+
# @param tool_approval [Symbol, nil] what to do with tool calls that require approval when no
|
|
155
|
+
# on_tool_approval is given: :deny (default) or :approve
|
|
156
|
+
# @param on_tool_approval [Proc, nil] called with each RubyLLM::ToolCall that requires approval;
|
|
157
|
+
# a truthy return approves it
|
|
158
|
+
# @param on_input_request [Proc, nil] called with each RubyLLM::MCP::InputRequest an MCP server
|
|
159
|
+
# raises; return a Hash of field values (or true for a URL request) to answer, anything else declines
|
|
160
|
+
# @param mcp_discovery [Boolean] select only the MCP servers relevant to each message (see MCP::ServerDiscovery)
|
|
155
161
|
# @param enable_cache [Boolean] whether to enable semantic caching
|
|
156
162
|
# @param bus [TypedBus::MessageBus, nil] optional message bus for inter-robot communication
|
|
157
163
|
# @param temperature [Float, nil] controls randomness
|
|
@@ -174,12 +180,14 @@ module RobotLab
|
|
|
174
180
|
local_tools: [],
|
|
175
181
|
model: nil,
|
|
176
182
|
provider: nil,
|
|
177
|
-
mcp_servers: [],
|
|
178
183
|
mcp: :none,
|
|
179
184
|
tools: :none,
|
|
180
185
|
on_tool_call: nil,
|
|
181
186
|
on_tool_result: nil,
|
|
182
187
|
on_content: nil,
|
|
188
|
+
tool_approval: nil,
|
|
189
|
+
on_tool_approval: nil,
|
|
190
|
+
on_input_request: nil,
|
|
183
191
|
enable_cache: true,
|
|
184
192
|
bus: nil,
|
|
185
193
|
skills: nil,
|
|
@@ -210,7 +218,8 @@ module RobotLab
|
|
|
210
218
|
on_tool_call: on_tool_call, on_tool_result: on_tool_result,
|
|
211
219
|
on_content: on_content, bus: bus, enable_cache: enable_cache,
|
|
212
220
|
max_tool_rounds: max_tool_rounds, token_budget: token_budget, cost_budget: cost_budget,
|
|
213
|
-
doom_loop_threshold: doom_loop_threshold,
|
|
221
|
+
doom_loop_threshold: doom_loop_threshold, tool_approval: tool_approval,
|
|
222
|
+
on_tool_approval: on_tool_approval, on_input_request: on_input_request,
|
|
214
223
|
mcp: mcp, tools: tools, config: config
|
|
215
224
|
)
|
|
216
225
|
|
|
@@ -319,50 +328,9 @@ module RobotLab
|
|
|
319
328
|
self
|
|
320
329
|
end
|
|
321
330
|
|
|
322
|
-
#
|
|
323
|
-
#
|
|
324
|
-
#
|
|
325
|
-
#
|
|
326
|
-
# @return [self]
|
|
327
|
-
def connect_mcp!
|
|
328
|
-
resolved_mcp = resolve_mcp_hierarchy(@mcp_config)
|
|
329
|
-
ensure_mcp_clients(resolved_mcp) if resolved_mcp.is_a?(Array) && resolved_mcp.any?
|
|
330
|
-
self
|
|
331
|
-
end
|
|
332
|
-
|
|
333
|
-
# Returns server names that failed to connect.
|
|
334
|
-
#
|
|
335
|
-
# @return [Array<String>]
|
|
336
|
-
def failed_mcp_server_names
|
|
337
|
-
return [] unless @failed_mcp_configs
|
|
338
|
-
|
|
339
|
-
@failed_mcp_configs.keys
|
|
340
|
-
end
|
|
341
|
-
|
|
342
|
-
# Disconnect all MCP clients and bus channel.
|
|
343
|
-
#
|
|
344
|
-
# @return [self]
|
|
345
|
-
def disconnect
|
|
346
|
-
@mcp_clients.each_value(&:disconnect)
|
|
347
|
-
teardown_bus_channel if @bus
|
|
348
|
-
self
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
# --- Public APIs for external MCP and history management (A4) ---
|
|
352
|
-
|
|
353
|
-
# Inject pre-connected MCP clients and their tools into this robot.
|
|
354
|
-
# Used by host applications (e.g. AIA) that manage MCP connections
|
|
355
|
-
# externally and need to pass them to robots without re-connecting.
|
|
356
|
-
#
|
|
357
|
-
# @param clients [Hash<String, MCP::Client>] connected MCP clients by server name
|
|
358
|
-
# @param tools [Array<Tool>] tools discovered from the MCP servers
|
|
359
|
-
# @return [self]
|
|
360
|
-
def inject_mcp!(clients:, tools:)
|
|
361
|
-
@mcp_clients = clients
|
|
362
|
-
@mcp_tools = tools
|
|
363
|
-
@mcp_initialized = true
|
|
364
|
-
self
|
|
365
|
-
end
|
|
331
|
+
# --- Public APIs for external history management (A4) ---
|
|
332
|
+
# (MCP server management lives in Robot::MCPManagement: #mcp, #with_mcp,
|
|
333
|
+
# #connect_mcp!, #mcp_tools, #failed_mcp_server_names, #disconnect.)
|
|
366
334
|
|
|
367
335
|
# Access the underlying RubyLLM::Chat instance.
|
|
368
336
|
# Useful for checkpoint/restore operations that need direct
|
|
@@ -504,14 +472,6 @@ module RobotLab
|
|
|
504
472
|
nil
|
|
505
473
|
end
|
|
506
474
|
|
|
507
|
-
# Find an MCP client by server name.
|
|
508
|
-
#
|
|
509
|
-
# @param server_name [String] the MCP server name
|
|
510
|
-
# @return [MCP::Client, nil]
|
|
511
|
-
def mcp_client(server_name)
|
|
512
|
-
@mcp_clients[server_name]
|
|
513
|
-
end
|
|
514
|
-
|
|
515
475
|
# Add a learning to this robot's accumulation store.
|
|
516
476
|
#
|
|
517
477
|
# Deduplicates by bidirectional substring matching: a new learning is
|
|
@@ -566,10 +526,10 @@ module RobotLab
|
|
|
566
526
|
skills: @skills,
|
|
567
527
|
system_prompt: system_prompt,
|
|
568
528
|
local_tools: local_tools.map { |t| t.respond_to?(:name) ? t.name : t.to_s },
|
|
569
|
-
mcp_tools: mcp_tools.map(&:name),
|
|
570
529
|
mcp_config: @mcp_config,
|
|
571
530
|
tools_config: @tools_config,
|
|
572
|
-
mcp_servers:
|
|
531
|
+
mcp_servers: mcp.names,
|
|
532
|
+
mcp_failed: (failed_mcp_server_names if @mcp_failures.any?),
|
|
573
533
|
model: model,
|
|
574
534
|
config: (@config.empty? ? nil : @config.to_json_hash),
|
|
575
535
|
bus: @bus ? true : nil
|
|
@@ -621,7 +581,8 @@ module RobotLab
|
|
|
621
581
|
presence_penalty:, frequency_penalty:, stop:,
|
|
622
582
|
on_tool_call:, on_tool_result:, on_content:,
|
|
623
583
|
bus:, enable_cache:, max_tool_rounds:, token_budget:, cost_budget:,
|
|
624
|
-
doom_loop_threshold:,
|
|
584
|
+
doom_loop_threshold:, tool_approval:, on_tool_approval:, on_input_request:,
|
|
585
|
+
mcp:, tools:, config:)
|
|
625
586
|
explicit_fields = {
|
|
626
587
|
model: model, temperature: temperature, top_p: top_p, top_k: top_k,
|
|
627
588
|
max_tokens: max_tokens, presence_penalty: presence_penalty,
|
|
@@ -629,29 +590,33 @@ module RobotLab
|
|
|
629
590
|
on_tool_call: on_tool_call, on_tool_result: on_tool_result,
|
|
630
591
|
on_content: on_content, bus: bus, enable_cache: enable_cache,
|
|
631
592
|
max_tool_rounds: max_tool_rounds, token_budget: token_budget, cost_budget: cost_budget,
|
|
632
|
-
doom_loop_threshold: doom_loop_threshold
|
|
593
|
+
doom_loop_threshold: doom_loop_threshold, tool_approval: tool_approval,
|
|
594
|
+
on_tool_approval: on_tool_approval, on_input_request: on_input_request
|
|
633
595
|
}.compact
|
|
634
596
|
|
|
635
|
-
|
|
636
|
-
explicit_fields[:
|
|
637
|
-
explicit_fields[:tools] = tools unless ToolConfig.none_value?(tools)
|
|
597
|
+
explicit_fields[:mcp] = mcp unless ToolConfig.none_value?(mcp)
|
|
598
|
+
explicit_fields[:tools] = tools unless ToolConfig.none_value?(tools)
|
|
638
599
|
|
|
639
600
|
explicit_config = RunConfig.new(**explicit_fields)
|
|
640
601
|
@config = config ? config.merge(explicit_config) : explicit_config
|
|
641
602
|
end
|
|
642
603
|
|
|
643
604
|
def extract_config_ivars
|
|
644
|
-
@on_tool_call
|
|
645
|
-
@on_tool_result
|
|
646
|
-
@on_content
|
|
647
|
-
@
|
|
648
|
-
@
|
|
605
|
+
@on_tool_call = @config.on_tool_call
|
|
606
|
+
@on_tool_result = @config.on_tool_result
|
|
607
|
+
@on_content = @config.on_content
|
|
608
|
+
@on_tool_approval = @config.on_tool_approval
|
|
609
|
+
@on_input_request = @config.on_input_request
|
|
610
|
+
@tool_approval = @config.tool_approval || :deny
|
|
611
|
+
@mcp_config = @config.mcp || :none
|
|
612
|
+
@tools_config = @config.tools || :none
|
|
649
613
|
end
|
|
650
614
|
|
|
651
615
|
def initialize_runtime_state
|
|
652
|
-
@
|
|
653
|
-
@
|
|
654
|
-
@
|
|
616
|
+
@mcp_registry = MCP::Registry.new
|
|
617
|
+
@mcp_failures = {}
|
|
618
|
+
@mcp_tool_owners = {}.compare_by_identity
|
|
619
|
+
@run_mcp_servers = []
|
|
655
620
|
@bus = @config.bus
|
|
656
621
|
@message_counter = 0
|
|
657
622
|
@outbox = {}
|
|
@@ -745,15 +710,17 @@ module RobotLab
|
|
|
745
710
|
@chat.after_tool_result do |result|
|
|
746
711
|
@on_tool_result&.call(result)
|
|
747
712
|
end
|
|
713
|
+
install_tool_hook_bridge
|
|
748
714
|
setup_bus_channel if @bus
|
|
749
715
|
end
|
|
750
716
|
|
|
751
717
|
# Dynamically delegate all with_* methods from @chat, returning self for chaining.
|
|
752
718
|
# Discovered from the actual Chat class to avoid maintenance sync issues.
|
|
719
|
+
# with_mcp is the robot's own (Robot::MCPManagement), not the chat's.
|
|
753
720
|
# :reek:NestedIterators -- the inner block is the delegator method body being defined, not an iteration.
|
|
754
721
|
def define_chat_delegators
|
|
755
722
|
@chat.class.public_instance_methods(false)
|
|
756
|
-
.select { |m| m.start_with?('with_') }
|
|
723
|
+
.select { |m| m.start_with?('with_') && m != :with_mcp }
|
|
757
724
|
.each do |method_name|
|
|
758
725
|
define_singleton_method(method_name) do |*args, **kwargs, &block|
|
|
759
726
|
@chat.public_send(method_name, *args, **kwargs, &block)
|
|
@@ -776,16 +743,15 @@ module RobotLab
|
|
|
776
743
|
end
|
|
777
744
|
end
|
|
778
745
|
|
|
779
|
-
# :reek:TooManyStatements -- resolve/
|
|
746
|
+
# :reek:TooManyStatements -- resolve/select/connect/filter/attach is one linear per-turn tool pipeline.
|
|
780
747
|
def prepare_tools(message:, mcp:, tools:, network:, network_config:)
|
|
781
748
|
resolved_mcp = resolve_mcp_hierarchy(mcp, network: network, network_config: network_config)
|
|
782
749
|
resolved_tools = resolve_tools_hierarchy(tools, network: network, network_config: network_config)
|
|
783
750
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
ensure_mcp_clients(resolved_mcp)
|
|
751
|
+
# This turn's MCP servers: registered (and connected) on demand, chosen
|
|
752
|
+
# by the cascade and, with mcp_discovery, by relevance to the message.
|
|
753
|
+
@run_mcp_servers = servers_for(resolved_mcp, message: message)
|
|
754
|
+
available = @local_tools + collect_mcp_tools(@run_mcp_servers)
|
|
789
755
|
|
|
790
756
|
# An EXPLICIT `:none` (or literal `[]`) means "send zero tools this turn" —
|
|
791
757
|
# e.g. a relevance filter judged no tool useful for the prompt. That is
|
|
@@ -793,18 +759,22 @@ module RobotLab
|
|
|
793
759
|
# tool". Both collapse to `[]` once resolved, and `filtered_tools([])`
|
|
794
760
|
# treats an empty allowlist as "all tools", so we must branch on the raw
|
|
795
761
|
# runtime value before resolution to honor the zero-tools intent.
|
|
796
|
-
filtered = explicit_none_tools?(tools) ? [] : cap_tools(filtered_tools(resolved_tools))
|
|
762
|
+
filtered = explicit_none_tools?(tools) ? [] : cap_tools(filtered_tools(available, resolved_tools))
|
|
763
|
+
check_unique_tool_names!(filtered)
|
|
797
764
|
|
|
798
765
|
# Clear-then-add so the chat holds EXACTLY this turn's resolved+capped
|
|
799
766
|
# set. RubyLLM's with_tools appends; on a persistent chat that lets tools
|
|
800
767
|
# accumulate across turns, so a capped per-turn addition could still push
|
|
801
|
-
# the chat's total past the provider limit. with_tools(nil) is ruby_llm
|
|
802
|
-
#
|
|
803
|
-
# tools
|
|
804
|
-
|
|
768
|
+
# the chat's total past the provider limit. with_tools(nil) is ruby_llm's
|
|
769
|
+
# "clear the tool set"; an explicit none stops there, sending zero tools
|
|
770
|
+
# this turn. MCP tools a previous turn attached are cleared too, so a
|
|
771
|
+
# turn that selects no MCP servers sends none of their tools.
|
|
772
|
+
return unless filtered.any? || explicit_none_tools?(tools) || mcp_tools_attached?
|
|
805
773
|
|
|
774
|
+
deferred, offered = filtered.partition { |tool| deferred_mcp_tool?(tool) }
|
|
806
775
|
@chat.with_tools(nil)
|
|
807
|
-
@chat.with_tools(*
|
|
776
|
+
@chat.with_tools(*offered) if offered.any?
|
|
777
|
+
@chat.with_tools(*deferred, defer: true) if deferred.any?
|
|
808
778
|
end
|
|
809
779
|
|
|
810
780
|
# True when the runtime tools value explicitly requests zero tools — `:none`
|
|
@@ -872,7 +842,8 @@ module RobotLab
|
|
|
872
842
|
install_doom_loop_detection
|
|
873
843
|
ask_kwargs = kwargs.slice(:with)
|
|
874
844
|
streaming = effective_streaming_block(block)
|
|
875
|
-
ask(effective_message, **ask_kwargs, &streaming)
|
|
845
|
+
response = ask(effective_message, **ask_kwargs, &streaming)
|
|
846
|
+
settle_pending_tool_calls(response, &streaming)
|
|
876
847
|
end
|
|
877
848
|
end
|
|
878
849
|
|
|
@@ -938,20 +909,22 @@ module RobotLab
|
|
|
938
909
|
}
|
|
939
910
|
end
|
|
940
911
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
912
|
+
# Apply a name allowlist to this turn's available tools; an empty
|
|
913
|
+
# allowlist means "no filter".
|
|
914
|
+
#
|
|
915
|
+
# @param available [Array<RubyLLM::Tool>] local tools plus this turn's MCP tools
|
|
916
|
+
# @param allowed_names [Array<String>]
|
|
917
|
+
# @return [Array<RubyLLM::Tool>]
|
|
918
|
+
def filtered_tools(available, allowed_names)
|
|
947
919
|
return available if allowed_names.empty?
|
|
948
920
|
|
|
949
921
|
ToolConfig.filter_tools(available, allowed_names: allowed_names)
|
|
950
922
|
end
|
|
951
923
|
|
|
952
924
|
# Prepend accumulated learnings to a user message when learnings exist.
|
|
925
|
+
# Non-String requests (an MCP prompt, for instance) pass through untouched.
|
|
953
926
|
def inject_learnings(message)
|
|
954
|
-
return message if @learnings.empty? || message.
|
|
927
|
+
return message if @learnings.empty? || !message.is_a?(String)
|
|
955
928
|
|
|
956
929
|
learning_block = "LEARNINGS FROM PREVIOUS RUNS:\n" +
|
|
957
930
|
@learnings.map { |l| "- #{l}" }.join("\n") +
|
|
@@ -991,7 +964,7 @@ module RobotLab
|
|
|
991
964
|
# :reek:FeatureEnvy -- checking-then-removing on the chat's singleton class is one atomic operation.
|
|
992
965
|
def remove_doom_loop_detection
|
|
993
966
|
sc = @chat.singleton_class
|
|
994
|
-
sc.remove_method(:execute_tool) if sc.method_defined?(:execute_tool)
|
|
967
|
+
sc.remove_method(:execute_tool) if sc.method_defined?(:execute_tool, false)
|
|
995
968
|
end
|
|
996
969
|
|
|
997
970
|
# Compact conversation history before an ask() call if auto_compact is set.
|
data/lib/robot_lab/run_config.rb
CHANGED
|
@@ -39,11 +39,12 @@ module RobotLab
|
|
|
39
39
|
TOOL_FIELDS = %i[mcp tools].freeze
|
|
40
40
|
|
|
41
41
|
# Callback fields (Procs)
|
|
42
|
-
CALLBACK_FIELDS = %i[on_tool_call on_tool_result on_content].freeze
|
|
42
|
+
CALLBACK_FIELDS = %i[on_tool_call on_tool_result on_content on_tool_approval on_input_request].freeze
|
|
43
43
|
|
|
44
44
|
# Infrastructure fields
|
|
45
45
|
INFRA_FIELDS = %i[bus enable_cache max_tool_rounds token_budget cost_budget ractor_pool_size
|
|
46
|
-
max_concurrent_robots doom_loop_threshold auto_compact compact_threshold max_tools
|
|
46
|
+
max_concurrent_robots doom_loop_threshold auto_compact compact_threshold max_tools
|
|
47
|
+
tool_approval].freeze
|
|
47
48
|
|
|
48
49
|
# All recognized fields
|
|
49
50
|
FIELDS = (LLM_FIELDS + TOOL_FIELDS + CALLBACK_FIELDS + INFRA_FIELDS).freeze
|
data/lib/robot_lab/tool.rb
CHANGED
|
@@ -27,17 +27,13 @@ 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
|
|
30
|
+
# :reek:InstanceVariableAssumption -- @custom_name is set by Tool.create via instance_variable_set,
|
|
31
31
|
# @raise_on_error/@ractor_safe are class-level DSL ivars; every read is defined?-guarded.
|
|
32
32
|
class Tool < RubyLLM::Tool
|
|
33
33
|
# @!attribute [rw] robot
|
|
34
34
|
# @return [Robot, nil] the robot that owns this tool
|
|
35
35
|
attr_accessor :robot
|
|
36
36
|
|
|
37
|
-
# @!attribute [r] mcp
|
|
38
|
-
# @return [String, nil] the MCP server name if this is an MCP-provided tool
|
|
39
|
-
attr_reader :mcp
|
|
40
|
-
|
|
41
37
|
class << self
|
|
42
38
|
# When true, exceptions from #execute propagate instead of being caught.
|
|
43
39
|
# Default: false (graceful error handling).
|
|
@@ -111,26 +107,18 @@ module RobotLab
|
|
|
111
107
|
context.tool_result
|
|
112
108
|
end
|
|
113
109
|
|
|
114
|
-
# Override name to support explicit names for dynamic
|
|
110
|
+
# Override name to support explicit names for dynamic tools.
|
|
115
111
|
#
|
|
116
112
|
# @return [String] the tool name
|
|
117
113
|
def name
|
|
118
114
|
defined?(@custom_name) && @custom_name ? @custom_name : super
|
|
119
115
|
end
|
|
120
116
|
|
|
121
|
-
#
|
|
122
|
-
#
|
|
123
|
-
# @return [Boolean]
|
|
124
|
-
def mcp?
|
|
125
|
-
!@mcp.nil?
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
# Factory for dynamic tools (MCP wrappers, inline tools).
|
|
117
|
+
# Factory for dynamic (inline) tools.
|
|
129
118
|
#
|
|
130
119
|
# @param name [String, Symbol] the tool name
|
|
131
120
|
# @param description [String, nil] what the tool does
|
|
132
121
|
# @param parameters [Hash, nil] JSON Schema parameter definition
|
|
133
|
-
# @param mcp [String, nil] MCP server name
|
|
134
122
|
# @param robot [Robot, nil] the owning robot
|
|
135
123
|
# @yield [args] block that executes the tool logic
|
|
136
124
|
# @return [Tool] a new tool instance
|
|
@@ -141,16 +129,15 @@ module RobotLab
|
|
|
141
129
|
# description: "Get the current time"
|
|
142
130
|
# ) { |args| Time.now.to_s }
|
|
143
131
|
#
|
|
144
|
-
# @example
|
|
132
|
+
# @example With a parameter schema
|
|
145
133
|
# tool = RobotLab::Tool.create(
|
|
146
134
|
# name: "search",
|
|
147
135
|
# description: "Search the web",
|
|
148
|
-
# parameters: { type: "object", properties: { q: { type: "string" } }, required: ["q"] }
|
|
149
|
-
#
|
|
150
|
-
# ) { |args| mcp_client.call_tool("search", args) }
|
|
136
|
+
# parameters: { type: "object", properties: { q: { type: "string" } }, required: ["q"] }
|
|
137
|
+
# ) { |args| WebSearch.run(args[:q]) }
|
|
151
138
|
#
|
|
152
139
|
# :reek:TooManyStatements -- anonymous-class factory: DSL application and instance wiring form one linear build.
|
|
153
|
-
def self.create(name:, description: nil, parameters: nil,
|
|
140
|
+
def self.create(name:, description: nil, parameters: nil, robot: nil, &handler)
|
|
154
141
|
desc_text = description
|
|
155
142
|
params_hash = parameters
|
|
156
143
|
block = handler
|
|
@@ -169,7 +156,6 @@ module RobotLab
|
|
|
169
156
|
|
|
170
157
|
instance = tool_class.new(robot: robot)
|
|
171
158
|
instance.instance_variable_set(:@custom_name, name.to_s)
|
|
172
|
-
instance.instance_variable_set(:@mcp, mcp)
|
|
173
159
|
instance
|
|
174
160
|
end
|
|
175
161
|
|
|
@@ -192,8 +178,7 @@ module RobotLab
|
|
|
192
178
|
def to_h
|
|
193
179
|
{
|
|
194
180
|
name: name,
|
|
195
|
-
description: description
|
|
196
|
-
mcp: mcp
|
|
181
|
+
description: description
|
|
197
182
|
}.compact
|
|
198
183
|
end
|
|
199
184
|
|
data/lib/robot_lab/version.rb
CHANGED
data/lib/robot_lab.rb
CHANGED
|
@@ -53,10 +53,7 @@ loader.ignore("#{__dir__}/robot_lab/names.rb")
|
|
|
53
53
|
loader.inflector.inflect(
|
|
54
54
|
'robot_lab' => 'RobotLab',
|
|
55
55
|
'mcp' => 'MCP',
|
|
56
|
-
'openai' => 'OpenAI'
|
|
57
|
-
'sse' => 'SSE',
|
|
58
|
-
'streamable_http' => 'StreamableHTTP',
|
|
59
|
-
'websocket' => 'WebSocket'
|
|
56
|
+
'openai' => 'OpenAI'
|
|
60
57
|
)
|
|
61
58
|
|
|
62
59
|
loader.setup
|