legion-llm 0.6.16 → 0.6.18

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0fe6246015e9967dec5bbe428ab2cb5869b4a359c4a5b40c0a8206d0fe7b516d
4
- data.tar.gz: f803de963b1887d064506187c3d7c8fa00b437613f5456bcbf87adae1d53a3a4
3
+ metadata.gz: 83b1bf0eb338e47eb37627eb7004afb30b3fba91627577034554827edd797bcf
4
+ data.tar.gz: 54ba4b787dfb7ebbbe6ef10f363cb237075f5b37f74467c9408db723d9a36c48
5
5
  SHA512:
6
- metadata.gz: e7f65ec573b55b840b7b07b9a9c83c7aa189696b162936e63ad65ddf508be3240eb056fc70d3bbbad532eb2f3db841c4facd3efb1df30e9bf816b4f37d8a33e0
7
- data.tar.gz: 6a823d728ab547b3ccab050d7a01e9825cb3764446eecb2f32b71ce1c7ff73f7da7f167e9198b9a45f4941b7ed8113c3aac3f1ca95c0a76b46a5b502dd713147
6
+ metadata.gz: 1a546dca02b403bc025621c1a980cf081114c383f97bd3ce3eef245a7b3314cdaacc7328cbbbfd7879cbf95dd53069e48b9af8f2401099e68966e0ee4e0b624f
7
+ data.tar.gz: ddb6aabeca04cdbdacfd2412ee2713dfaecc4b17911b55246ec680538874df7725eabef8768fe69c8202952d6a3f14d0a0ae27696cd64ebf2727679f7947cd61
data/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ### Fixed
6
+ - fix Process namespace collision by using ::Process::CLOCK_MONOTONIC prefix inside Legion namespace
7
+
8
+ ### Added
9
+ - `Legion::LLM::Pipeline::ToolAdapter` - wraps Tools::Base for RubyLLM sessions
10
+
11
+ ### Changed
12
+ - Renamed `McpToolAdapter` to `ToolAdapter` (backwards compat alias kept)
13
+ - Pipeline step `McpDiscovery` renamed to `ToolDiscovery`
14
+ - Executor reads from `Legion::Tools::Registry`
15
+ - `chat_single` wraps registry tools with ToolAdapter
16
+ - Routes: executor handles all tool injection, routes only pass client tools
17
+
18
+ ### Removed
19
+ - MCP server dependency for tool injection in routes
20
+
5
21
  ## [0.6.16] - 2026-04-03
6
22
 
7
23
  ### Fixed
data/CLAUDE.md CHANGED
@@ -8,7 +8,7 @@
8
8
  Core LegionIO gem providing LLM capabilities to all extensions. Wraps ruby_llm to provide a consistent interface for chat, embeddings, tool use, and agents across multiple providers (Bedrock, Anthropic, OpenAI, Gemini, Ollama). Includes a dynamic weighted routing engine that dispatches requests across local, fleet, and cloud tiers based on caller intent, priority rules, time schedules, cost multipliers, and real-time provider health.
9
9
 
10
10
  **GitHub**: https://github.com/LegionIO/legion-llm
11
- **Version**: 0.5.15
11
+ **Version**: 0.6.18
12
12
  **License**: Apache-2.0
13
13
 
14
14
  ## Architecture
@@ -61,8 +61,12 @@ Legion::LLM (lib/legion/llm.rb)
61
61
  │ ├── Timeline # Ordered event recording with participant tracking
62
62
  │ ├── Executor # 18-step pipeline skeleton with profile-aware execution
63
63
  │ ├── Steps/
64
- │ │ └── Metering # Metering event builder (absorbed from lex-llm-gateway)
64
+ │ │ ├── Metering # Metering event builder (absorbed from lex-llm-gateway)
65
+ │ │ └── ToolDiscovery # Step 9 — formerly McpDiscovery; renamed to ToolDiscovery (McpDiscovery kept as backwards alias)
65
66
  │ └── Executor#call_stream # Streaming variant: pre-provider steps, yield chunks, post-provider steps
67
+
68
+ │ Note: Legion::LLM::ToolRegistry was removed. Tool registration now lives in Legion::Tools::Registry (LegionIO gem).
69
+ │ McpToolAdapter renamed to ToolAdapter; McpToolAdapter kept as a backwards-compatible alias.
66
70
  ├── CostEstimator # Model cost estimation with fuzzy pricing (absorbed from lex-llm-gateway)
67
71
  ├── Fleet # Fleet RPC dispatch (absorbed from lex-llm-gateway)
68
72
  │ ├── Dispatcher # Fleet dispatch with timeout and availability checks
@@ -18,7 +18,7 @@ module Legion
18
18
  :audit, :warnings, :discovered_tools, :confidence_score,
19
19
  :escalation_chain
20
20
 
21
- include Steps::McpDiscovery
21
+ include Steps::ToolDiscovery
22
22
  include Steps::ToolCalls
23
23
  include Steps::KnowledgeCapture
24
24
  include Steps::ConfidenceScoring
@@ -28,14 +28,14 @@ module Legion
28
28
 
29
29
  STEPS = %i[
30
30
  tracing_init idempotency conversation_uuid context_load
31
- rbac classification billing gaia_advisory tier_assignment rag_context mcp_discovery
31
+ rbac classification billing gaia_advisory tier_assignment rag_context tool_discovery
32
32
  routing request_normalization token_budget provider_call response_normalization
33
33
  debate confidence_scoring tool_calls context_store post_response knowledge_capture response_return
34
34
  ].freeze
35
35
 
36
36
  PRE_PROVIDER_STEPS = %i[
37
37
  tracing_init idempotency conversation_uuid context_load
38
- rbac classification billing gaia_advisory tier_assignment rag_context mcp_discovery
38
+ rbac classification billing gaia_advisory tier_assignment rag_context tool_discovery
39
39
  routing request_normalization token_budget
40
40
  ].freeze
41
41
 
@@ -45,27 +45,6 @@ module Legion
45
45
 
46
46
  ASYNC_SAFE_STEPS = %i[post_response knowledge_capture response_return].freeze
47
47
 
48
- ALWAYS_LOADED_MCP_TOOLS = %w[
49
- legion_do
50
- legion_get_status
51
- legion_run_task
52
- legion_describe_runner
53
- legion_list_extensions
54
- legion_get_extension
55
- legion_list_tasks
56
- legion_get_task
57
- legion_get_task_logs
58
- legion_query_knowledge
59
- legion_knowledge_health
60
- legion_knowledge_context
61
- legion_list_workers
62
- legion_show_worker
63
- legion_mesh_status
64
- legion_list_peers
65
- legion_tools
66
- legion_search_sessions
67
- ].freeze
68
-
69
48
  ASYNC_THREAD_POOL = Concurrent::FixedThreadPool.new(4, fallback_policy: :caller_runs)
70
49
 
71
50
  def initialize(request)
@@ -104,27 +83,42 @@ module Legion
104
83
 
105
84
  private
106
85
 
107
- def inject_discovered_tools(session)
108
- server = mcp_server
109
- return unless server.respond_to?(:tool_registry)
86
+ def inject_registry_tools(session)
87
+ return unless defined?(::Legion::Tools::Registry)
110
88
 
111
- requested = requested_deferred_tool_names
112
89
  injected_names = []
113
90
 
114
- server.tool_registry.each do |mcp_tool_class|
115
- adapter = McpToolAdapter.new(mcp_tool_class)
116
- next unless ALWAYS_LOADED_MCP_TOOLS.include?(adapter.name) || requested.include?(adapter.name)
117
-
91
+ # Always-loaded tools — inject all unconditionally
92
+ ::Legion::Tools::Registry.tools.each do |tool_class|
93
+ adapter = ToolAdapter.new(tool_class)
118
94
  session.with_tool(adapter)
119
95
  injected_names << adapter.name
120
96
  rescue StandardError => e
121
- @warnings << "Failed to inject tool: #{e.message}"
122
- handle_exception(e, level: :warn, operation: 'llm.pipeline.inject_tool')
97
+ @warnings << "Failed to inject always tool: #{e.message}"
98
+ handle_exception(e, level: :warn, operation: 'llm.pipeline.inject_always_tool')
99
+ end
100
+
101
+ # Requested deferred tools — inject only if explicitly requested
102
+ deferred = ::Legion::Tools::Registry.respond_to?(:deferred_tools) ? ::Legion::Tools::Registry.deferred_tools : []
103
+ requested = requested_deferred_tool_names
104
+ if requested.any?
105
+ deferred.each do |tool_class|
106
+ adapter = ToolAdapter.new(tool_class)
107
+ next unless requested.include?(adapter.name)
108
+
109
+ session.with_tool(adapter)
110
+ injected_names << adapter.name
111
+ rescue StandardError => e
112
+ @warnings << "Failed to inject deferred tool: #{e.message}"
113
+ handle_exception(e, level: :warn, operation: 'llm.pipeline.inject_deferred_tool')
114
+ end
123
115
  end
124
116
 
125
117
  log.info(
126
- "[llm][mcp] inject request_id=#{@request.id} " \
127
- "always_loaded=#{ALWAYS_LOADED_MCP_TOOLS.size} requested_deferred=#{requested.size} " \
118
+ "[llm][tools] inject request_id=#{@request.id} " \
119
+ "always=#{::Legion::Tools::Registry.tools.size} " \
120
+ "deferred_available=#{deferred.size} " \
121
+ "requested_deferred=#{requested.size} " \
128
122
  "injected=#{injected_names.size} names=#{injected_names.first(25).join(',')}"
129
123
  )
130
124
  rescue StandardError => e
@@ -132,6 +126,9 @@ module Legion
132
126
  handle_exception(e, level: :warn, operation: 'llm.pipeline.inject_tools')
133
127
  end
134
128
 
129
+ # Backwards compatibility alias
130
+ alias inject_discovered_tools inject_registry_tools
131
+
135
132
  def execute_steps
136
133
  executed = 0
137
134
  skipped = 0
@@ -678,8 +675,7 @@ module Legion
678
675
  session.with_tool(tool)
679
676
  end
680
677
 
681
- ToolRegistry.tools.each { |tool| session.with_tool(tool) } if defined?(ToolRegistry)
682
- inject_discovered_tools(session)
678
+ inject_registry_tools(session)
683
679
  end
684
680
 
685
681
  def apply_ruby_llm_instructions(session)
@@ -864,23 +860,6 @@ module Legion
864
860
  )
865
861
  end
866
862
 
867
- def mcp_server
868
- return ::Legion::MCP.server if defined?(::Legion::MCP) && ::Legion::MCP.respond_to?(:server)
869
-
870
- require 'legion/mcp'
871
- return unless defined?(::Legion::MCP) && ::Legion::MCP.respond_to?(:server)
872
-
873
- ::Legion::MCP.server
874
- rescue LoadError => e
875
- @warnings << "MCP unavailable: #{e.message}"
876
- handle_exception(e, level: :debug, operation: 'llm.pipeline.mcp_server.require')
877
- nil
878
- rescue StandardError => e
879
- @warnings << "MCP server load error: #{e.message}"
880
- handle_exception(e, level: :warn, operation: 'llm.pipeline.mcp_server')
881
- nil
882
- end
883
-
884
863
  def requested_deferred_tool_names
885
864
  metadata = @request.metadata || {}
886
865
  requested = metadata[:requested_tools] || metadata['requested_tools'] || []
@@ -1,69 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ruby_llm'
4
- require 'legion/logging/helper'
5
-
6
- module Legion
7
- module LLM
8
- module Pipeline
9
- class McpToolAdapter < RubyLLM::Tool
10
- include Legion::Logging::Helper
11
-
12
- def initialize(mcp_tool_class)
13
- @mcp_tool_class = mcp_tool_class
14
- raw_name = mcp_tool_class.respond_to?(:tool_name) ? mcp_tool_class.tool_name : mcp_tool_class.name.to_s
15
- @tool_name = raw_name.tr('.', '_')
16
- @tool_desc = mcp_tool_class.respond_to?(:description) ? mcp_tool_class.description.to_s : ''
17
- @tool_schema = mcp_tool_class.respond_to?(:input_schema) ? mcp_tool_class.input_schema : nil
18
- super()
19
- end
20
-
21
- def name
22
- @tool_name
23
- end
24
-
25
- def description
26
- @tool_desc
27
- end
28
-
29
- def params_schema
30
- return @params_schema if defined?(@params_schema)
31
-
32
- @params_schema = (RubyLLM::Utils.deep_stringify_keys(@tool_schema) if @tool_schema.is_a?(Hash))
33
- end
34
-
35
- def execute(**args)
36
- log.info("[llm][tools] adapter.execute name=#{@tool_name} arguments=#{summarize_payload(args)}")
37
- result = @mcp_tool_class.call(**args)
38
- content = extract_content(result)
39
- log.info("[llm][tools] adapter.result name=#{@tool_name} output=#{summarize_payload(content)}")
40
- content
41
- rescue StandardError => e
42
- handle_exception(e, level: :warn, operation: 'llm.pipeline.mcp_tool_adapter.execute', tool_name: @tool_name)
43
- "Tool error: #{e.message}"
44
- end
45
-
46
- private
47
-
48
- def extract_content(result)
49
- # MCP::Tool::Response — has .content array of {type: 'text', text: '...'}
50
- if result.respond_to?(:content) && result.content.is_a?(Array)
51
- result.content.filter_map { |c| c[:text] || c['text'] || c.to_s }.join("\n")
52
- elsif result.is_a?(Hash) && result[:content].is_a?(Array)
53
- result[:content].filter_map { |c| c[:text] || c['text'] }.join("\n")
54
- elsif result.is_a?(Hash)
55
- Legion::JSON.dump(result)
56
- elsif result.is_a?(String)
57
- result
58
- else
59
- result.to_s
60
- end
61
- end
62
-
63
- def summarize_payload(payload)
64
- payload.to_s[0, 200].inspect
65
- end
66
- end
67
- end
68
- end
69
- end
3
+ # Backwards-compatibility shim — the implementation moved to tool_adapter.rb.
4
+ # Callers that require this path directly will still find McpToolAdapter via the alias.
5
+ require_relative 'tool_adapter'
@@ -6,18 +6,18 @@ module Legion
6
6
  module Profile
7
7
  GAIA_SKIP = %i[
8
8
  idempotency conversation_uuid context_load rbac classification
9
- billing gaia_advisory mcp_discovery context_store post_response
9
+ billing gaia_advisory tool_discovery context_store post_response
10
10
  ].freeze
11
11
 
12
12
  SYSTEM_SKIP = %i[
13
13
  idempotency conversation_uuid context_load rbac classification
14
- billing gaia_advisory rag_context mcp_discovery context_store
14
+ billing gaia_advisory rag_context tool_discovery context_store
15
15
  post_response
16
16
  ].freeze
17
17
 
18
18
  QUICK_REPLY_SKIP = %i[
19
19
  idempotency conversation_uuid context_load classification
20
- gaia_advisory rag_context mcp_discovery confidence_scoring
20
+ gaia_advisory rag_context tool_discovery confidence_scoring
21
21
  tool_calls context_store post_response knowledge_capture
22
22
  ].freeze
23
23
 
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/logging/helper'
4
+
5
+ module Legion
6
+ module LLM
7
+ module Pipeline
8
+ module Steps
9
+ module ToolDiscovery
10
+ include Legion::Logging::Helper
11
+
12
+ def step_tool_discovery
13
+ @discovered_tools ||= []
14
+ start_time = Time.now
15
+
16
+ discover_registry_tools
17
+ discover_client_tools
18
+
19
+ total = @discovered_tools.size
20
+ if total.positive?
21
+ sources = @discovered_tools.filter_map { |t| t.dig(:source, :server) || t.dig(:source, :type) }.uniq
22
+ @enrichments['tool:discovery'] = {
23
+ content: "#{total} tools from #{sources.length} sources",
24
+ data: { tool_count: total, sources: sources },
25
+ timestamp: Time.now
26
+ }
27
+ end
28
+
29
+ record_tool_discovery_timeline(total, start_time)
30
+ rescue StandardError => e
31
+ @warnings << "Tool discovery error: #{e.message}"
32
+ handle_exception(e, level: :warn, operation: 'llm.pipeline.steps.tool_discovery')
33
+ record_tool_discovery_timeline(0)
34
+ end
35
+
36
+ # Backwards compatibility alias — step name used in STEPS array is tool_discovery
37
+ alias step_mcp_discovery step_tool_discovery
38
+
39
+ private
40
+
41
+ def discover_registry_tools
42
+ return unless defined?(::Legion::Tools::Registry)
43
+
44
+ ::Legion::Tools::Registry.tools.each do |tool_class|
45
+ name = tool_class.respond_to?(:tool_name) ? tool_class.tool_name : tool_class.name
46
+ desc = tool_class.respond_to?(:description) ? tool_class.description : ''
47
+ schema = tool_class.respond_to?(:input_schema) ? tool_class.input_schema : {}
48
+ @discovered_tools << {
49
+ name: name,
50
+ description: desc,
51
+ parameters: schema,
52
+ source: { type: :registry, server: 'legion' }
53
+ }
54
+ end
55
+
56
+ log.info(
57
+ "[llm][tools] discover request_id=#{@request.id} " \
58
+ "registry_tools=#{::Legion::Tools::Registry.tools.size}"
59
+ )
60
+ rescue StandardError => e
61
+ @warnings << "Registry tool discovery error: #{e.message}"
62
+ handle_exception(e, level: :warn, operation: 'llm.pipeline.steps.tool_discovery.registry')
63
+ end
64
+
65
+ def discover_client_tools
66
+ return unless defined?(::Legion::MCP::Client::Pool)
67
+
68
+ ::Legion::MCP::Client::Pool.all_tools.each do |tool|
69
+ @discovered_tools << {
70
+ name: tool[:name],
71
+ description: tool[:description],
72
+ parameters: tool[:input_schema],
73
+ source: tool[:source]
74
+ }
75
+ end
76
+ rescue StandardError => e
77
+ @warnings << "Client tool discovery error: #{e.message}"
78
+ handle_exception(e, level: :warn, operation: 'llm.pipeline.steps.tool_discovery.client')
79
+ end
80
+
81
+ def record_tool_discovery_timeline(count, start_time = nil)
82
+ duration = start_time ? ((Time.now - start_time) * 1000).to_i : 0
83
+ @timeline.record(
84
+ category: :enrichment, key: 'tool:discovery',
85
+ direction: :inbound, detail: "#{count} tools discovered",
86
+ from: 'tool_registry', to: 'pipeline',
87
+ duration_ms: duration
88
+ )
89
+ end
90
+ end
91
+
92
+ # Backwards compatibility alias
93
+ McpDiscovery = ToolDiscovery
94
+ end
95
+ end
96
+ end
97
+ end
@@ -17,7 +17,7 @@ require_relative 'steps/billing'
17
17
  require_relative 'steps/gaia_advisory'
18
18
  require_relative 'steps/tier_assigner'
19
19
  require_relative 'steps/post_response'
20
- require_relative 'steps/mcp_discovery'
20
+ require_relative 'steps/tool_discovery'
21
21
  require_relative 'steps/tool_calls'
22
22
  require_relative 'steps/rag_context'
23
23
  require_relative 'steps/rag_guard'
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ruby_llm'
4
+ require 'legion/logging/helper'
5
+
6
+ module Legion
7
+ module LLM
8
+ module Pipeline
9
+ class ToolAdapter < RubyLLM::Tool
10
+ include Legion::Logging::Helper
11
+
12
+ MAX_TOOL_NAME_LENGTH = 64
13
+
14
+ def initialize(tool_class)
15
+ @tool_class = tool_class
16
+ raw_name = tool_class.respond_to?(:tool_name) ? tool_class.tool_name : tool_class.name.to_s
17
+ @tool_name = sanitize_tool_name(raw_name)
18
+ @tool_desc = tool_class.respond_to?(:description) ? tool_class.description.to_s : ''
19
+ @tool_schema = tool_class.respond_to?(:input_schema) ? tool_class.input_schema : nil
20
+ super()
21
+ end
22
+
23
+ def name
24
+ @tool_name
25
+ end
26
+
27
+ def description
28
+ @tool_desc
29
+ end
30
+
31
+ def params_schema
32
+ return @params_schema if defined?(@params_schema)
33
+
34
+ @params_schema = (RubyLLM::Utils.deep_stringify_keys(@tool_schema) if @tool_schema.is_a?(Hash))
35
+ end
36
+
37
+ def execute(**args)
38
+ log.info("[llm][tools] adapter.execute name=#{@tool_name} arguments=#{summarize_payload(args)}")
39
+ result = @tool_class.call(**args)
40
+ content = extract_content(result)
41
+ log.info("[llm][tools] adapter.result name=#{@tool_name} output=#{summarize_payload(content)}")
42
+ content
43
+ rescue StandardError => e
44
+ handle_exception(e, level: :warn, operation: 'llm.pipeline.tool_adapter.execute', tool_name: @tool_name)
45
+ "Tool error: #{e.message}"
46
+ end
47
+
48
+ private
49
+
50
+ def extract_content(result)
51
+ # MCP::Tool::Response — has .content array of {type: 'text', text: '...'}
52
+ if result.respond_to?(:content) && result.content.is_a?(Array)
53
+ result.content.filter_map { |c| c[:text] || c['text'] || c.to_s }.join("\n")
54
+ elsif result.is_a?(Hash) && result[:content].is_a?(Array)
55
+ result[:content].filter_map { |c| c[:text] || c['text'] }.join("\n")
56
+ elsif result.is_a?(Hash)
57
+ Legion::JSON.dump(result)
58
+ elsif result.is_a?(String)
59
+ result
60
+ else
61
+ result.to_s
62
+ end
63
+ end
64
+
65
+ def summarize_payload(payload)
66
+ payload.to_s[0, 200].inspect
67
+ end
68
+
69
+ # Bedrock constraints: [a-zA-Z0-9_-]+ and max 64 chars.
70
+ # Falls back to a stable name derived from the class object_id if sanitization yields
71
+ # an empty string (e.g. all chars stripped), ensuring the result always satisfies the
72
+ # at-least-one-character requirement.
73
+ def sanitize_tool_name(raw)
74
+ name = raw.tr('.', '_')
75
+ name = name.gsub(/[^a-zA-Z0-9_-]/, '') # strip ?, !, etc.
76
+ name = name[0, MAX_TOOL_NAME_LENGTH] if name.length > MAX_TOOL_NAME_LENGTH
77
+ name.empty? ? "tool_#{@tool_class.object_id}" : name
78
+ end
79
+ end
80
+
81
+ # Backwards compatibility alias
82
+ McpToolAdapter = ToolAdapter
83
+ end
84
+ end
85
+ end
@@ -11,7 +11,7 @@ require_relative 'pipeline/gaia_caller'
11
11
  require_relative 'pipeline/tool_dispatcher'
12
12
  require_relative 'pipeline/enrichment_injector'
13
13
  require_relative 'pipeline/steps'
14
- require_relative 'pipeline/mcp_tool_adapter'
14
+ require_relative 'pipeline/tool_adapter'
15
15
  require_relative 'pipeline/executor'
16
16
 
17
17
  module Legion
@@ -12,17 +12,6 @@ require 'open3'
12
12
  require 'time'
13
13
  require 'legion/logging/helper'
14
14
 
15
- begin
16
- require 'legion/cli/chat/tools/search_traces'
17
- if defined?(Legion::LLM::ToolRegistry) && defined?(Legion::CLI::Chat::Tools::SearchTraces)
18
- Legion::LLM::ToolRegistry.register(Legion::CLI::Chat::Tools::SearchTraces)
19
- end
20
- rescue LoadError => e
21
- if defined?(Legion::Logging) && Legion::Logging.respond_to?(:log_exception)
22
- Legion::Logging.log_exception(e, payload_summary: 'SearchTraces not available for API', component_type: :api)
23
- end
24
- end
25
-
26
15
  module Legion
27
16
  module LLM
28
17
  module Routes
@@ -214,34 +203,6 @@ module Legion
214
203
  end
215
204
  # rubocop:enable Metrics/BlockLength
216
205
 
217
- define_method(:cached_mcp_tools) do
218
- @@llm_cached_mcp_tools ||= begin # rubocop:disable Style/ClassVars
219
- all = []
220
- if defined?(Legion::MCP) && Legion::MCP.respond_to?(:server)
221
- Legion::MCP.server
222
- if defined?(Legion::MCP::Server) && Legion::MCP::Server.respond_to?(:tool_registry)
223
- require 'legion/llm/pipeline/mcp_tool_adapter' unless defined?(Legion::LLM::Pipeline::McpToolAdapter)
224
- Legion::MCP::Server.tool_registry.each do |tc|
225
- all << Legion::LLM::Pipeline::McpToolAdapter.new(tc)
226
- rescue StandardError
227
- nil
228
- end
229
- end
230
- end
231
- always_names = %w[legion_do legion_get_status legion_run_task legion_describe_runner
232
- legion_list_extensions legion_get_extension legion_list_tasks
233
- legion_get_task legion_get_task_logs legion_query_knowledge
234
- legion_knowledge_health legion_knowledge_context legion_list_workers
235
- legion_show_worker legion_mesh_status legion_list_peers
236
- legion_tools legion_search_sessions]
237
- {
238
- always: all.select { |t| always_names.include?(t.name) }.freeze,
239
- deferred: all.reject { |t| always_names.include?(t.name) }.freeze,
240
- all: all.freeze
241
- }.freeze
242
- end
243
- end
244
-
245
206
  define_method(:extract_tool_calls) do |pipeline_response|
246
207
  tools_data = pipeline_response.tools
247
208
  return [] unless tools_data.is_a?(Array) && !tools_data.empty?
@@ -505,16 +466,7 @@ module Legion
505
466
  build_client_tool_class(ts[:name].to_s, ts[:description].to_s, ts[:parameters] || ts[:input_schema])
506
467
  end
507
468
 
508
- # Inject daemon MCP tools alongside client tools
509
- all_tools = tool_declarations.dup
510
- begin
511
- mcp_tools = cached_mcp_tools
512
- mcp_to_inject = Array(requested_tools).empty? ? mcp_tools[:always] : mcp_tools[:all]
513
- all_tools.concat(mcp_to_inject) if mcp_to_inject&.any?
514
- log.unknown "[llm][api] inference tools client=#{tool_declarations.size} mcp=#{mcp_to_inject&.size || 0} total=#{all_tools.size}"
515
- rescue StandardError => e
516
- handle_exception(e, level: :warn, operation: 'llm.routes.mcp_tool_injection', request_id: request_id)
517
- end
469
+ log.unknown "[llm][api] inference tools client=#{tool_declarations.size}"
518
470
 
519
471
  streaming = body[:stream] == true && request.preferred_type.to_s.include?('text/event-stream')
520
472
  normalized_caller = caller_context.respond_to?(:transform_keys) ? caller_context.transform_keys(&:to_sym) : {}
@@ -541,7 +493,7 @@ module Legion
541
493
  messages: messages,
542
494
  system: body[:system],
543
495
  routing: { provider: provider, model: model },
544
- tools: all_tools,
496
+ tools: tool_declarations,
545
497
  caller: effective_caller,
546
498
  conversation_id: conversation_id,
547
499
  metadata: { requested_tools: requested_tools },
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module LLM
5
- VERSION = '0.6.16'
5
+ VERSION = '0.6.18'
6
6
  end
7
7
  end
data/lib/legion/llm.rb CHANGED
@@ -32,7 +32,6 @@ require_relative 'llm/scheduling'
32
32
  require_relative 'llm/off_peak'
33
33
  require_relative 'llm/cost_tracker'
34
34
  require_relative 'llm/token_tracker'
35
- require_relative 'llm/tool_registry'
36
35
  require_relative 'llm/override_confidence'
37
36
  require_relative 'llm/routes'
38
37
 
@@ -108,7 +107,7 @@ module Legion
108
107
  # for automatic metering and fleet dispatch
109
108
  def chat(model: nil, provider: nil, intent: nil, tier: nil, escalate: nil,
110
109
  max_escalations: nil, quality_check: nil, message: nil, **kwargs, &)
111
- started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
110
+ started_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
112
111
  log_inference_request(
113
112
  request_type: :chat,
114
113
  requested_model: model,
@@ -154,7 +153,7 @@ module Legion
154
153
  # Send a single message — daemon-first, falls through to direct on unavailability.
155
154
  def ask(message:, model: nil, provider: nil, intent: nil, tier: nil,
156
155
  context: {}, identity: nil, &)
157
- started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
156
+ started_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
158
157
  log_inference_request(
159
158
  request_type: :ask,
160
159
  requested_model: model,
@@ -367,7 +366,7 @@ module Legion
367
366
  end
368
367
 
369
368
  def elapsed_ms_since(started_at)
370
- ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round
369
+ ((::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - started_at) * 1000).round
371
370
  end
372
371
 
373
372
  def inference_input_payload(message:, messages:)
@@ -644,7 +643,7 @@ module Legion
644
643
 
645
644
  def chat_single(model:, provider:, intent:, tier:, message: nil, **kwargs, &block)
646
645
  explicit_tools = kwargs.delete(:tools)
647
- tools = explicit_tools || ToolRegistry.tools
646
+ tools = explicit_tools || adapted_registry_tools
648
647
  tools = nil if tools.empty?
649
648
 
650
649
  if (intent || tier) && Router.routing_enabled?
@@ -687,6 +686,24 @@ module Legion
687
686
  response
688
687
  end
689
688
 
689
+ def adapted_registry_tools
690
+ tool_classes = if defined?(::Legion::Tools::Registry)
691
+ ::Legion::Tools::Registry.tools
692
+ else
693
+ return []
694
+ end
695
+
696
+ tool_classes.map do |tool_class|
697
+ Pipeline::ToolAdapter.new(tool_class)
698
+ rescue StandardError => e
699
+ handle_exception(e, level: :warn, operation: 'llm.adapted_registry_tools', tool_class: tool_class.to_s)
700
+ nil
701
+ end.compact
702
+ rescue StandardError => e
703
+ handle_exception(e, level: :warn, operation: 'llm.adapted_registry_tools')
704
+ []
705
+ end
706
+
690
707
  def try_defer(intent:, urgency:, model:, provider:, message:, **)
691
708
  return nil unless Scheduling.enabled? && Scheduling.should_defer?(intent: intent || :normal, urgency: urgency)
692
709
  return nil unless defined?(Batch) && Batch.enabled?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.16
4
+ version: 0.6.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -287,7 +287,9 @@ files:
287
287
  - lib/legion/llm/pipeline/steps/tier_assigner.rb
288
288
  - lib/legion/llm/pipeline/steps/token_budget.rb
289
289
  - lib/legion/llm/pipeline/steps/tool_calls.rb
290
+ - lib/legion/llm/pipeline/steps/tool_discovery.rb
290
291
  - lib/legion/llm/pipeline/timeline.rb
292
+ - lib/legion/llm/pipeline/tool_adapter.rb
291
293
  - lib/legion/llm/pipeline/tool_dispatcher.rb
292
294
  - lib/legion/llm/pipeline/tracing.rb
293
295
  - lib/legion/llm/provider_registry.rb
@@ -306,7 +308,6 @@ files:
306
308
  - lib/legion/llm/shadow_eval.rb
307
309
  - lib/legion/llm/structured_output.rb
308
310
  - lib/legion/llm/token_tracker.rb
309
- - lib/legion/llm/tool_registry.rb
310
311
  - lib/legion/llm/transport/exchanges/audit.rb
311
312
  - lib/legion/llm/transport/exchanges/escalation.rb
312
313
  - lib/legion/llm/transport/messages/audit_event.rb
@@ -1,40 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'legion/logging/helper'
4
-
5
- module Legion
6
- module LLM
7
- module ToolRegistry
8
- extend Legion::Logging::Helper
9
-
10
- @tools = []
11
- @mutex = Mutex.new
12
-
13
- class << self
14
- def register(tool_class)
15
- registered = @mutex.synchronize do
16
- next false if @tools.include?(tool_class)
17
-
18
- @tools << tool_class
19
- true
20
- end
21
- if registered
22
- log.info("[llm][tools] registered class=#{tool_class}")
23
- else
24
- log.debug("[llm][tools] already_registered class=#{tool_class}")
25
- end
26
- end
27
-
28
- def tools
29
- @mutex.synchronize { @tools.dup }
30
- end
31
-
32
- def clear
33
- count = @mutex.synchronize { @tools.size }
34
- @mutex.synchronize { @tools.clear }
35
- log.info("[llm][tools] registry_cleared count=#{count}")
36
- end
37
- end
38
- end
39
- end
40
- end