legionio 1.7.16 → 1.7.17
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 +19 -0
- data/lib/legion/api/llm.rb +5 -85
- data/lib/legion/cli/do_command.rb +76 -37
- data/lib/legion/extensions/builders/runners.rb +6 -0
- data/lib/legion/extensions/core.rb +8 -0
- data/lib/legion/extensions.rb +11 -42
- data/lib/legion/service.rb +32 -3
- data/lib/legion/tools/base.rb +87 -0
- data/lib/legion/tools/config.rb +64 -0
- data/lib/legion/tools/discovery.rb +190 -0
- data/lib/legion/tools/do.rb +151 -0
- data/lib/legion/tools/embedding_cache/migrations/001_create_tool_embedding_cache.rb +15 -0
- data/lib/legion/tools/embedding_cache.rb +417 -0
- data/lib/legion/tools/registry.rb +76 -0
- data/lib/legion/tools/status.rb +50 -0
- data/lib/legion/tools.rb +38 -0
- data/lib/legion/version.rb +1 -1
- data/lib/legion.rb +1 -0
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6fc7545d6fe4e3dc832eba2265dc27fed7546fb3d770dec04b4375b35505479f
|
|
4
|
+
data.tar.gz: 513b2ff1093560b1d88c427b176d6f27959a6c94abd3106173d462d29f8cd69c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b6de374974924d397249494b4aed6ff9e99263116a36d47226530c12653d672371565d060b65913d249fce4d0ed808fd6ffae45a669a6e4b1b67d87a056dcbe8
|
|
7
|
+
data.tar.gz: 2d64efa40365a2da40fcc97de5173e2c3bfd3458e8adf861a0efde9c35514d7b876ab200f2d5648ca68f08f5de879c29dde2c4d5c3f53d9c2240ae8f4575debe
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
### Added
|
|
6
|
+
- `Legion::Tools::Base` - canonical tool base class with DSL
|
|
7
|
+
- `Legion::Tools::Registry` - always/deferred tool classification
|
|
8
|
+
- `Legion::Tools::Discovery` - auto-discovers tools from extension runners with hierarchical DSL
|
|
9
|
+
- `Legion::Tools::EmbeddingCache` - 5-tier persistent embedding cache (L0 memory + Cache + Data)
|
|
10
|
+
- `mcp_tools?` and `mcp_tools_deferred?` extension Core DSL
|
|
11
|
+
- `runner_modules` accessor on extension builders
|
|
12
|
+
- `loaded_extension_modules` accessor on `Legion::Extensions`
|
|
13
|
+
- Static tools: `Do`, `Status`, `Config` with `Legion::Logging::Helper`
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Boot registers tools into Tools::Registry after extension load
|
|
17
|
+
- Embedding index build is async (non-blocking)
|
|
18
|
+
- API inference reads from Tools::Registry instead of MCP
|
|
19
|
+
- Capability registration methods are now no-ops (replaced by Tools::Discovery)
|
|
20
|
+
|
|
21
|
+
### Removed
|
|
22
|
+
- Direct MCP dependency for tool access in API inference
|
|
23
|
+
|
|
5
24
|
## [1.7.16] - 2026-04-03
|
|
6
25
|
|
|
7
26
|
### Fixed
|
data/lib/legion/api/llm.rb
CHANGED
|
@@ -3,36 +3,6 @@
|
|
|
3
3
|
require 'securerandom'
|
|
4
4
|
require 'open3'
|
|
5
5
|
|
|
6
|
-
begin
|
|
7
|
-
require 'legion/cli/chat/tools/search_traces'
|
|
8
|
-
if defined?(Legion::LLM::ToolRegistry) && defined?(Legion::CLI::Chat::Tools::SearchTraces)
|
|
9
|
-
Legion::LLM::ToolRegistry.register(Legion::CLI::Chat::Tools::SearchTraces)
|
|
10
|
-
end
|
|
11
|
-
rescue LoadError => e
|
|
12
|
-
Legion::Logging.log_exception(e, payload_summary: 'SearchTraces not available for API', component_type: :api) if defined?(Legion::Logging)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
ALWAYS_LOADED_TOOLS = %w[
|
|
16
|
-
legion_do
|
|
17
|
-
legion_get_status
|
|
18
|
-
legion_run_task
|
|
19
|
-
legion_describe_runner
|
|
20
|
-
legion_list_extensions
|
|
21
|
-
legion_get_extension
|
|
22
|
-
legion_list_tasks
|
|
23
|
-
legion_get_task
|
|
24
|
-
legion_get_task_logs
|
|
25
|
-
legion_query_knowledge
|
|
26
|
-
legion_knowledge_health
|
|
27
|
-
legion_knowledge_context
|
|
28
|
-
legion_list_workers
|
|
29
|
-
legion_show_worker
|
|
30
|
-
legion_mesh_status
|
|
31
|
-
legion_list_peers
|
|
32
|
-
legion_tools
|
|
33
|
-
legion_search_sessions
|
|
34
|
-
].freeze
|
|
35
|
-
|
|
36
6
|
module Legion
|
|
37
7
|
class API < Sinatra::Base
|
|
38
8
|
module Routes
|
|
@@ -59,44 +29,6 @@ module Legion
|
|
|
59
29
|
defined?(Legion::Extensions::LLM::Gateway::Runners::Inference)
|
|
60
30
|
end
|
|
61
31
|
|
|
62
|
-
define_method(:cached_mcp_tools) do
|
|
63
|
-
@@cached_mcp_tools ||= begin # rubocop:disable Style/ClassVars
|
|
64
|
-
all = []
|
|
65
|
-
begin
|
|
66
|
-
require 'legion/mcp' unless defined?(Legion::MCP) && Legion::MCP.respond_to?(:server)
|
|
67
|
-
Legion::MCP.server if defined?(Legion::MCP) && Legion::MCP.respond_to?(:server)
|
|
68
|
-
rescue LoadError => e
|
|
69
|
-
Legion::Logging.log_exception(e, payload_summary: 'cached_mcp_tools: failed to require legion/mcp', component_type: :api)
|
|
70
|
-
end
|
|
71
|
-
if defined?(Legion::MCP::Server) && Legion::MCP::Server.respond_to?(:tool_registry)
|
|
72
|
-
require 'legion/llm/pipeline/mcp_tool_adapter' unless defined?(Legion::LLM::Pipeline::McpToolAdapter)
|
|
73
|
-
Legion::Logging.info "[llm][api] cached_mcp_tools building from #{Legion::MCP::Server.tool_registry.size} MCP tools"
|
|
74
|
-
Legion::MCP::Server.tool_registry.each do |tc|
|
|
75
|
-
all << Legion::LLM::Pipeline::McpToolAdapter.new(tc)
|
|
76
|
-
rescue StandardError => e
|
|
77
|
-
Legion::Logging.log_exception(e, payload_summary: "cached_mcp_tools: failed to adapt #{tc}", component_type: :api)
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
{
|
|
81
|
-
always: all.select { |t| ALWAYS_LOADED_TOOLS.include?(t.name) }.freeze,
|
|
82
|
-
deferred: all.reject { |t| ALWAYS_LOADED_TOOLS.include?(t.name) }.freeze,
|
|
83
|
-
all: all.freeze
|
|
84
|
-
}.freeze
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
define_method(:inject_mcp_tools) do |session, requested_tools: []|
|
|
89
|
-
cache = cached_mcp_tools
|
|
90
|
-
cache[:always].each { |t| session.with_tool(t) }
|
|
91
|
-
|
|
92
|
-
return if requested_tools.empty?
|
|
93
|
-
|
|
94
|
-
requested = requested_tools.map { |n| n.to_s.tr('.', '_') }
|
|
95
|
-
cache[:deferred].each do |t|
|
|
96
|
-
session.with_tool(t) if requested.include?(t.name)
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
32
|
define_method(:build_client_tool_class) do |tname, tdesc, tschema|
|
|
101
33
|
klass = Class.new(RubyLLM::Tool) do
|
|
102
34
|
description tdesc
|
|
@@ -185,7 +117,7 @@ module Legion
|
|
|
185
117
|
|
|
186
118
|
message = body[:message]
|
|
187
119
|
|
|
188
|
-
# Tier 0 check
|
|
120
|
+
# Tier 0 check - serve from PatternStore if available
|
|
189
121
|
if defined?(Legion::MCP::TierRouter)
|
|
190
122
|
tier_result = Legion::MCP::TierRouter.route(
|
|
191
123
|
intent: message,
|
|
@@ -206,8 +138,7 @@ module Legion
|
|
|
206
138
|
model = body[:model]
|
|
207
139
|
provider = body[:provider]
|
|
208
140
|
|
|
209
|
-
# Route through full Legion pipeline when gateway is available
|
|
210
|
-
# Ingress -> RBAC -> Events -> Task -> Gateway (metering + fleet) -> LLM
|
|
141
|
+
# Route through full Legion pipeline when gateway is available
|
|
211
142
|
if gateway_available?
|
|
212
143
|
ingress_result = Legion::Ingress.run(
|
|
213
144
|
payload: { message: message, model: model, provider: provider,
|
|
@@ -315,7 +246,7 @@ module Legion
|
|
|
315
246
|
|
|
316
247
|
caller_identity = env['legion.tenant_id'] || 'api:inference'
|
|
317
248
|
|
|
318
|
-
# GAIA bridge
|
|
249
|
+
# GAIA bridge - push InputFrame to sensory buffer
|
|
319
250
|
last_user = messages.select { |m| (m[:role] || m['role']).to_s == 'user' }.last
|
|
320
251
|
prompt = (last_user || {})[:content] || (last_user || {})['content'] || ''
|
|
321
252
|
|
|
@@ -345,18 +276,7 @@ module Legion
|
|
|
345
276
|
# Detect streaming mode
|
|
346
277
|
streaming = body[:stream] == true && env['HTTP_ACCEPT']&.include?('text/event-stream')
|
|
347
278
|
|
|
348
|
-
#
|
|
349
|
-
all_tools = tool_classes.dup
|
|
350
|
-
begin
|
|
351
|
-
mcp_cache = cached_mcp_tools
|
|
352
|
-
mcp_to_inject = requested_tools.empty? ? mcp_cache[:always] : mcp_cache[:all]
|
|
353
|
-
all_tools.concat(mcp_to_inject) if mcp_to_inject&.any?
|
|
354
|
-
Legion::Logging.debug "[llm][api] inference mcp_injected=#{mcp_to_inject&.size || 0} total_tools=#{all_tools.size}"
|
|
355
|
-
rescue StandardError => e
|
|
356
|
-
Legion::Logging.log_exception(e, payload_summary: 'mcp tool injection failed', component_type: :api)
|
|
357
|
-
end
|
|
358
|
-
|
|
359
|
-
# Build pipeline request
|
|
279
|
+
# Executor handles all registry tool injection — API only passes client-defined tools
|
|
360
280
|
require 'legion/llm/pipeline/request' unless defined?(Legion::LLM::Pipeline::Request)
|
|
361
281
|
require 'legion/llm/pipeline/executor' unless defined?(Legion::LLM::Pipeline::Executor)
|
|
362
282
|
|
|
@@ -364,7 +284,7 @@ module Legion
|
|
|
364
284
|
messages: messages,
|
|
365
285
|
system: body[:system],
|
|
366
286
|
routing: { provider: provider, model: model },
|
|
367
|
-
tools:
|
|
287
|
+
tools: tool_classes,
|
|
368
288
|
caller: { requested_by: { identity: caller_identity, type: :user, credential: :api } },
|
|
369
289
|
conversation_id: body[:conversation_id],
|
|
370
290
|
metadata: { requested_tools: requested_tools },
|
|
@@ -15,7 +15,7 @@ module Legion
|
|
|
15
15
|
result = try_daemon(intent, options) || try_in_process(intent) || try_llm_classify(intent)
|
|
16
16
|
|
|
17
17
|
if result.nil?
|
|
18
|
-
formatter.error('No matching
|
|
18
|
+
formatter.error('No matching tool found')
|
|
19
19
|
formatter.detail('Try: legion lex list (to see available extensions)')
|
|
20
20
|
raise SystemExit, 1
|
|
21
21
|
end
|
|
@@ -53,37 +53,73 @@ module Legion
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def try_in_process(intent)
|
|
56
|
-
return nil unless defined?(Legion::
|
|
56
|
+
return nil unless defined?(Legion::Tools::Registry)
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
matched = Legion::Tools::Registry.all_tools.find do |t|
|
|
59
|
+
t.tool_name.include?(intent.downcase.tr(' ', '_')) ||
|
|
60
|
+
t.description.downcase.include?(intent.downcase)
|
|
61
|
+
end
|
|
62
|
+
return nil unless matched
|
|
63
|
+
|
|
64
|
+
begin
|
|
65
|
+
result = matched.call
|
|
66
|
+
normalize_in_process_result(result, matched.tool_name)
|
|
67
|
+
rescue ArgumentError
|
|
68
|
+
{ matched: matched.tool_name, status: 'requires_daemon',
|
|
69
|
+
note: 'Tool requires arguments; start the daemon and retry: legion start' }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def normalize_in_process_result(result, tool_name)
|
|
74
|
+
return { matched: tool_name, result: result } unless result.is_a?(Hash)
|
|
75
|
+
|
|
76
|
+
normalized = result.dup
|
|
77
|
+
normalized[:matched] = tool_name
|
|
78
|
+
extracted = extract_tool_text(normalized)
|
|
60
79
|
|
|
61
|
-
|
|
62
|
-
|
|
80
|
+
if normalized[:error] == true
|
|
81
|
+
normalized[:error] = extracted.empty? ? 'Tool execution failed' : extracted
|
|
82
|
+
elsif !normalized.key?(:result) && !extracted.empty?
|
|
83
|
+
normalized[:result] = extracted
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
normalized
|
|
87
|
+
end
|
|
63
88
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
89
|
+
def extract_tool_text(value)
|
|
90
|
+
case value
|
|
91
|
+
when Hash
|
|
92
|
+
error_val = value[:error] || value['error']
|
|
93
|
+
return error_val.to_s unless error_val == true || error_val.nil? || error_val.to_s.empty?
|
|
94
|
+
|
|
95
|
+
%i[message result response detail content].each do |key|
|
|
96
|
+
extracted = extract_tool_text(value[key] || value[key.to_s])
|
|
97
|
+
return extracted unless extracted.empty?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
''
|
|
101
|
+
when Array
|
|
102
|
+
value.filter_map do |item|
|
|
103
|
+
text = extract_tool_text(item)
|
|
104
|
+
text unless text.empty?
|
|
105
|
+
end.join("\n")
|
|
106
|
+
when String
|
|
107
|
+
value.strip
|
|
71
108
|
else
|
|
72
|
-
|
|
73
|
-
status: 'resolved', note: 'Daemon not running; cannot execute. Start with: legion start' }
|
|
109
|
+
value.nil? ? '' : value.to_s
|
|
74
110
|
end
|
|
75
111
|
end
|
|
76
112
|
|
|
77
113
|
def try_llm_classify(intent)
|
|
78
|
-
return nil unless defined?(Legion::
|
|
114
|
+
return nil unless defined?(Legion::Tools::Registry) && defined?(Legion::LLM)
|
|
79
115
|
|
|
80
|
-
|
|
81
|
-
return nil if
|
|
116
|
+
tools = Legion::Tools::Registry.all_tools
|
|
117
|
+
return nil if tools.empty?
|
|
82
118
|
|
|
83
|
-
catalog =
|
|
84
|
-
prompt = "Given these
|
|
85
|
-
"Which
|
|
86
|
-
'Reply with ONLY the
|
|
119
|
+
catalog = tools.map { |t| "#{t.tool_name}: #{t.description}" }
|
|
120
|
+
prompt = "Given these tools:\n#{catalog.join("\n")}\n\n" \
|
|
121
|
+
"Which tool best matches this intent: \"#{intent}\"?\n" \
|
|
122
|
+
'Reply with ONLY the tool name (e.g., legion.do). ' \
|
|
87
123
|
'If none match, reply NONE.'
|
|
88
124
|
|
|
89
125
|
response = Legion::LLM.ask(
|
|
@@ -93,12 +129,10 @@ module Legion
|
|
|
93
129
|
chosen = response.is_a?(Hash) ? response[:response].to_s.strip : response.to_s.strip
|
|
94
130
|
return nil if chosen.empty? || chosen.upcase == 'NONE'
|
|
95
131
|
|
|
96
|
-
|
|
97
|
-
return nil unless
|
|
132
|
+
tool = Legion::Tools::Registry.find(chosen)
|
|
133
|
+
return nil unless tool
|
|
98
134
|
|
|
99
|
-
|
|
100
|
-
{ matched: cap.name, runner_class: runner_class, function: cap.function,
|
|
101
|
-
status: 'resolved', source: 'llm',
|
|
135
|
+
{ matched: tool.tool_name, status: 'resolved', source: 'llm',
|
|
102
136
|
note: 'Daemon not running; cannot execute. Start with: legion start' }
|
|
103
137
|
rescue StandardError => e
|
|
104
138
|
Legion::Logging.debug("DoCommand#try_llm_classify failed: #{e.message}") if defined?(Legion::Logging)
|
|
@@ -106,26 +140,31 @@ module Legion
|
|
|
106
140
|
end
|
|
107
141
|
|
|
108
142
|
def resolve_runner_class(intent)
|
|
109
|
-
return nil unless defined?(Legion::
|
|
143
|
+
return nil unless defined?(Legion::Tools::Registry)
|
|
110
144
|
|
|
111
|
-
|
|
112
|
-
|
|
145
|
+
matched = Legion::Tools::Registry.all_tools.find do |t|
|
|
146
|
+
t.description.downcase.include?(intent.downcase)
|
|
147
|
+
end
|
|
148
|
+
return nil unless matched.respond_to?(:extension) && matched.respond_to?(:runner)
|
|
113
149
|
|
|
114
|
-
build_runner_class(
|
|
150
|
+
build_runner_class(matched.extension, matched.runner)
|
|
115
151
|
end
|
|
116
152
|
|
|
117
153
|
def resolve_function(intent)
|
|
118
|
-
return nil unless defined?(Legion::
|
|
154
|
+
return nil unless defined?(Legion::Tools::Registry)
|
|
119
155
|
|
|
120
|
-
|
|
121
|
-
|
|
156
|
+
matched = Legion::Tools::Registry.all_tools.find do |t|
|
|
157
|
+
t.description.downcase.include?(intent.downcase)
|
|
158
|
+
end
|
|
159
|
+
return nil unless matched
|
|
122
160
|
|
|
123
|
-
|
|
161
|
+
matched.tool_name.split('.').last
|
|
124
162
|
end
|
|
125
163
|
|
|
126
164
|
def build_runner_class(extension, runner)
|
|
127
|
-
ext_part = extension.delete_prefix('lex-').split(/[-_]/).map(&:capitalize).join
|
|
128
|
-
|
|
165
|
+
ext_part = extension.to_s.delete_prefix('lex-').split(/[-_]/).map(&:capitalize).join
|
|
166
|
+
runner_part = runner.to_s.split('_').map(&:capitalize).join
|
|
167
|
+
"Legion::Extensions::#{ext_part}::Runners::#{runner_part}"
|
|
129
168
|
end
|
|
130
169
|
|
|
131
170
|
def daemon_port(options)
|
|
@@ -59,6 +59,12 @@ module Legion
|
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
+
def runner_modules
|
|
63
|
+
return [] unless defined?(@runners) && @runners.is_a?(Hash)
|
|
64
|
+
|
|
65
|
+
@runners.values.filter_map { |r| r[:runner_module] }
|
|
66
|
+
end
|
|
67
|
+
|
|
62
68
|
def runner_files
|
|
63
69
|
@runner_files ||= find_files('runners')
|
|
64
70
|
end
|
|
@@ -112,6 +112,14 @@ module Legion
|
|
|
112
112
|
true
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
+
def mcp_tools?
|
|
116
|
+
true
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def mcp_tools_deferred?
|
|
120
|
+
true
|
|
121
|
+
end
|
|
122
|
+
|
|
115
123
|
# Auto-generate AMQP message classes for each runner method that has a definition.
|
|
116
124
|
# Explicit Messages::* classes in the transport directory take precedence.
|
|
117
125
|
# Runs after build_runners so definitions are populated.
|
data/lib/legion/extensions.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'legion/extensions/core'
|
|
4
|
-
require 'legion/extensions/capability'
|
|
5
4
|
require 'legion/extensions/catalog'
|
|
6
5
|
require 'legion/extensions/permissions'
|
|
7
6
|
require 'legion/runner'
|
|
@@ -567,54 +566,24 @@ module Legion
|
|
|
567
566
|
|
|
568
567
|
public
|
|
569
568
|
|
|
570
|
-
def
|
|
571
|
-
|
|
572
|
-
|
|
569
|
+
def loaded_extension_modules
|
|
570
|
+
constants(false).filter_map do |const_name|
|
|
571
|
+
mod = const_get(const_name, false)
|
|
572
|
+
next nil unless mod.is_a?(Module) && mod.respond_to?(:runner_modules)
|
|
573
573
|
|
|
574
|
-
|
|
575
|
-
absorbers.each_value do |absorber_meta|
|
|
576
|
-
cap = Extensions::Capability.from_absorber(
|
|
577
|
-
extension: gem_name,
|
|
578
|
-
absorber: absorber_meta[:absorber_module],
|
|
579
|
-
patterns: absorber_meta[:patterns],
|
|
580
|
-
description: absorber_meta[:description]
|
|
581
|
-
)
|
|
582
|
-
Extensions::Catalog::Registry.register(cap)
|
|
574
|
+
mod
|
|
583
575
|
rescue StandardError => e
|
|
584
|
-
if defined?(Legion::Logging)
|
|
585
|
-
|
|
586
|
-
"Absorber catalog registration error for #{gem_name} " \
|
|
587
|
-
"(#{absorber_meta[:absorber_module]}): #{e.message}"
|
|
588
|
-
)
|
|
589
|
-
end
|
|
576
|
+
Legion::Logging.warn("[Extensions] loaded_extension_modules: #{e.message}") if defined?(Legion::Logging)
|
|
577
|
+
nil
|
|
590
578
|
end
|
|
591
579
|
end
|
|
592
580
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
runner_name = runner_meta[:runner_name]
|
|
596
|
-
(runner_meta[:class_methods] || {}).each do |fn_name, fn_meta|
|
|
597
|
-
next if fn_name.to_s.start_with?('_')
|
|
581
|
+
# Legacy capability registration - now handled by Tools::Discovery
|
|
582
|
+
def unregister_capabilities(_gem_name); end
|
|
598
583
|
|
|
599
|
-
|
|
600
|
-
(fn_meta[:args] || []).each do |arg|
|
|
601
|
-
type, name = arg
|
|
602
|
-
params[name] = { type: :string, required: type == :keyreq }
|
|
603
|
-
end
|
|
584
|
+
def register_absorber_capabilities(_gem_name, _absorbers); end
|
|
604
585
|
|
|
605
|
-
|
|
606
|
-
extension: gem_name,
|
|
607
|
-
runner: runner_name.to_s.split('_').map(&:capitalize).join,
|
|
608
|
-
function: fn_name.to_s,
|
|
609
|
-
parameters: params,
|
|
610
|
-
tags: [gem_name.delete_prefix('lex-')]
|
|
611
|
-
)
|
|
612
|
-
Extensions::Catalog::Registry.register(cap)
|
|
613
|
-
end
|
|
614
|
-
rescue StandardError => e
|
|
615
|
-
Legion::Logging.warn("Catalog registration error for #{gem_name}: #{e.message}") if defined?(Legion::Logging)
|
|
616
|
-
end
|
|
617
|
-
end
|
|
586
|
+
def register_capabilities(_gem_name, _runners); end
|
|
618
587
|
|
|
619
588
|
def gem_load(entry)
|
|
620
589
|
gem_name = entry[:gem_name]
|
data/lib/legion/service.rb
CHANGED
|
@@ -149,6 +149,8 @@ module Legion
|
|
|
149
149
|
setup_generated_functions
|
|
150
150
|
end
|
|
151
151
|
|
|
152
|
+
register_core_tools
|
|
153
|
+
|
|
152
154
|
Legion::Gaia.registry&.rediscover if gaia && defined?(Legion::Gaia) && Legion::Gaia.started?
|
|
153
155
|
|
|
154
156
|
Legion::Extensions::Agentic::Memory::Trace::Helpers::ErrorTracer.setup if defined?(Legion::Extensions::Agentic::Memory::Trace::Helpers::ErrorTracer)
|
|
@@ -159,10 +161,11 @@ module Legion
|
|
|
159
161
|
setup_metrics
|
|
160
162
|
setup_task_outcome_observer
|
|
161
163
|
|
|
162
|
-
# Pre-warm MCP server in background
|
|
164
|
+
# Pre-warm MCP server in background; async embedding build
|
|
163
165
|
Thread.new do
|
|
164
166
|
require 'legion/mcp' if defined?(Legion::Settings) && !defined?(Legion::MCP)
|
|
165
167
|
Legion::MCP.server if defined?(Legion::MCP) && Legion::MCP.respond_to?(:server)
|
|
168
|
+
Legion::MCP::Server.populate_embedding_index if defined?(Legion::MCP::Server) && Legion::MCP::Server.respond_to?(:populate_embedding_index)
|
|
166
169
|
rescue StandardError => e
|
|
167
170
|
log.warn("MCP pre-warm failed: #{e.message}")
|
|
168
171
|
end
|
|
@@ -607,7 +610,7 @@ module Legion
|
|
|
607
610
|
handle_exception(e, level: :warn, operation: 'service.shutdown_api')
|
|
608
611
|
end
|
|
609
612
|
|
|
610
|
-
def shutdown
|
|
613
|
+
def shutdown # rubocop:disable Metrics/CyclomaticComplexity
|
|
611
614
|
log.info('Legion::Service.shutdown was called')
|
|
612
615
|
@shutdown = true
|
|
613
616
|
Legion::Settings[:client][:shutting_down] = true
|
|
@@ -631,6 +634,8 @@ module Legion
|
|
|
631
634
|
|
|
632
635
|
shutdown_component('Dispatch') { Legion::Dispatch.shutdown } if defined?(Legion::Dispatch)
|
|
633
636
|
|
|
637
|
+
Legion::Tools::Registry.clear if defined?(Legion::Tools::Registry)
|
|
638
|
+
|
|
634
639
|
ext_timeout = Legion::Settings.dig(:extensions, :shutdown_timeout) || 15
|
|
635
640
|
shutdown_component('Extensions', timeout: ext_timeout) { Legion::Extensions.shutdown }
|
|
636
641
|
Legion::Readiness.mark_not_ready(:extensions)
|
|
@@ -665,7 +670,7 @@ module Legion
|
|
|
665
670
|
Legion::Events.emit('service.shutdown')
|
|
666
671
|
end
|
|
667
672
|
|
|
668
|
-
def reload # rubocop:disable Metrics/MethodLength
|
|
673
|
+
def reload # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
669
674
|
return if @reloading
|
|
670
675
|
|
|
671
676
|
@reloading = true
|
|
@@ -680,6 +685,9 @@ module Legion
|
|
|
680
685
|
Legion::Readiness.mark_not_ready(:gaia)
|
|
681
686
|
end
|
|
682
687
|
|
|
688
|
+
Legion::Tools::Registry.clear if defined?(Legion::Tools::Registry)
|
|
689
|
+
Legion::Tools::EmbeddingCache.clear_memory if defined?(Legion::Tools::EmbeddingCache) && Legion::Tools::EmbeddingCache.respond_to?(:clear_memory)
|
|
690
|
+
|
|
683
691
|
ext_timeout = Legion::Settings.dig(:extensions, :shutdown_timeout) || 15
|
|
684
692
|
shutdown_component('Extensions', timeout: ext_timeout) { Legion::Extensions.shutdown }
|
|
685
693
|
Legion::Readiness.mark_not_ready(:extensions)
|
|
@@ -727,8 +735,15 @@ module Legion
|
|
|
727
735
|
load_extensions
|
|
728
736
|
Legion::Readiness.mark_ready(:extensions)
|
|
729
737
|
|
|
738
|
+
register_core_tools
|
|
739
|
+
|
|
730
740
|
Legion::Crypt.cs
|
|
731
741
|
setup_api if @api_enabled
|
|
742
|
+
|
|
743
|
+
if defined?(Legion::MCP)
|
|
744
|
+
Legion::MCP.reset!
|
|
745
|
+
Legion::MCP.server if Legion::MCP.respond_to?(:server)
|
|
746
|
+
end
|
|
732
747
|
setup_network_watchdog
|
|
733
748
|
Legion::Settings[:client][:ready] = true
|
|
734
749
|
Legion::Events.emit('service.ready')
|
|
@@ -742,6 +757,20 @@ module Legion
|
|
|
742
757
|
Legion::Extensions.hook_extensions
|
|
743
758
|
end
|
|
744
759
|
|
|
760
|
+
def register_core_tools
|
|
761
|
+
require 'legion/tools'
|
|
762
|
+
Legion::Tools.register_all
|
|
763
|
+
Legion::Tools::Discovery.discover_and_register
|
|
764
|
+
Legion::Tools::EmbeddingCache.setup
|
|
765
|
+
|
|
766
|
+
log.info(
|
|
767
|
+
"Tools registered: #{Legion::Tools::Registry.tools.size} always, " \
|
|
768
|
+
"#{Legion::Tools::Registry.deferred_tools.size} deferred"
|
|
769
|
+
)
|
|
770
|
+
rescue StandardError => e
|
|
771
|
+
handle_exception(e, level: :warn, operation: 'service.register_core_tools')
|
|
772
|
+
end
|
|
773
|
+
|
|
745
774
|
def setup_generated_functions
|
|
746
775
|
return unless defined?(Legion::Extensions::Codegen::Helpers::GeneratedRegistry)
|
|
747
776
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Tools
|
|
5
|
+
class Base
|
|
6
|
+
class << self
|
|
7
|
+
# Lazy delegation instead of include Helper — Base loads at require time
|
|
8
|
+
# before Settings is initialized; Helper#log builds TaggedLogger which
|
|
9
|
+
# calls derive_log_segments -> Settings -> possible recursion.
|
|
10
|
+
# Subclass static tools (Do, Status, Config) CAN include Helper safely.
|
|
11
|
+
def log
|
|
12
|
+
Legion::Logging.respond_to?(:logger) ? Legion::Logging.logger : nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def handle_exception(err, **opts)
|
|
16
|
+
log&.warn("[Legion::Tools] #{opts[:operation] || 'unknown'}: #{err.message}")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def tool_name(name = nil)
|
|
20
|
+
name ? @tool_name = name : @tool_name
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def description(desc = nil)
|
|
24
|
+
desc ? @description = desc : (@description || '')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def input_schema(schema = nil)
|
|
28
|
+
schema ? @input_schema = schema : @input_schema
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def deferred(val = nil)
|
|
32
|
+
return @deferred || false if val.nil?
|
|
33
|
+
|
|
34
|
+
@deferred = val
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def deferred?
|
|
38
|
+
deferred
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Metadata that replaces Capability - Tools::Registry IS the catalog
|
|
42
|
+
def extension(val = nil)
|
|
43
|
+
return @extension if val.nil?
|
|
44
|
+
|
|
45
|
+
@extension = val
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def runner(val = nil)
|
|
49
|
+
return @runner if val.nil?
|
|
50
|
+
|
|
51
|
+
@runner = val
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def tags(val = nil)
|
|
55
|
+
return @tags || [] if val.nil?
|
|
56
|
+
|
|
57
|
+
@tags = val
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def mcp_category(val = nil)
|
|
61
|
+
return @mcp_category if val.nil?
|
|
62
|
+
|
|
63
|
+
@mcp_category = val
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def mcp_tier(val = nil)
|
|
67
|
+
return @mcp_tier if val.nil?
|
|
68
|
+
|
|
69
|
+
@mcp_tier = val
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def call(**_args)
|
|
73
|
+
raise NotImplementedError, "#{name} must implement .call"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def text_response(data)
|
|
77
|
+
text = data.is_a?(String) ? data : Legion::JSON.dump(data)
|
|
78
|
+
{ content: [{ type: 'text', text: text }] }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def error_response(msg)
|
|
82
|
+
{ content: [{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|