legionio 1.7.17 → 1.7.19

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: 6fc7545d6fe4e3dc832eba2265dc27fed7546fb3d770dec04b4375b35505479f
4
- data.tar.gz: 513b2ff1093560b1d88c427b176d6f27959a6c94abd3106173d462d29f8cd69c
3
+ metadata.gz: c2114c2b32ae4dbc1650bef546391f6abb1499a0b9d96d577739503c47caba79
4
+ data.tar.gz: 1e95267b23de0b635ffb32c9f745f58fe0bac63aa7d806a6581e3557b9b1aad6
5
5
  SHA512:
6
- metadata.gz: b6de374974924d397249494b4aed6ff9e99263116a36d47226530c12653d672371565d060b65913d249fce4d0ed808fd6ffae45a669a6e4b1b67d87a056dcbe8
7
- data.tar.gz: 2d64efa40365a2da40fcc97de5173e2c3bfd3458e8adf861a0efde9c35514d7b876ab200f2d5648ca68f08f5de879c29dde2c4d5c3f53d9c2240ae8f4575debe
6
+ metadata.gz: 56c140c38d73a16c97574b405c4f27bdb91de199758692e76aecd0ec55afad66acab08fc011cf31a658df408cfcf8506e5e2971f4fbe92a4276e8856153a7481
7
+ data.tar.gz: 903faf2b78e2f27e1176e03558138f58ee9e6bdfcc34ac3f61c8c933424a0f7a0c641ba97d38d27c527b41cad93c9d8f69bffb64fed8a032a5d1aeb2bd6cb5ff
data/CHANGELOG.md CHANGED
@@ -2,6 +2,32 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.7.19] - 2026-04-06
6
+
7
+ ### Added
8
+ - `ALWAYS_LOADED` constant in `Tools::Discovery` — pins apollo/knowledge and eval/evaluation runners to always-loaded regardless of extension DSL
9
+ - `always_loaded_names` method on `Tools::Registry` returning names of all non-deferred registered tools
10
+
11
+ ### Changed
12
+ - Tool name format changed from dot-separated to dash-separated (`legion-ext-runner-func`) for LLM provider compatibility
13
+ - Reduced noisy debug logging in `Tools::Discovery` and `Tools::Registry`
14
+
15
+ ## [1.7.18] - 2026-04-06
16
+
17
+ ### Added
18
+ - Multi-phase extension loading: identity providers (`lex-identity-*`) load in phase 0 before all other extensions in phase 1
19
+ - `identity` category in extension registry with prefix matching for `lex-identity-*` gems at tier 0, phase 0
20
+ - `group_by_phase` method groups discovered extensions by phase from the category registry
21
+ - `load_phase_extensions` replaces `load_extensions` — scopes parallel loading to a subset of entries per phase
22
+ - `hook_phase_actors` replaces `hook_all_actors` — hooks deferred actors after each phase completes
23
+ - Per-phase logging during extension loading shows cumulative actor counts
24
+
25
+ ### Changed
26
+ - `hook_extensions` now iterates phases sequentially (phase 0 then phase 1), running full load+hook cycle per phase
27
+ - `default_category_registry` includes `phase:` key on all categories; all non-identity categories default to phase 1
28
+ - Catalog transitions (`transition(:running)` + `flush_persisted_transitions`) happen after all phases complete
29
+ - Reserved prefixes list now includes `identity`
30
+
5
31
  ### Added
6
32
  - `Legion::Tools::Base` - canonical tool base class with DSL
7
33
  - `Legion::Tools::Registry` - always/deferred tool classification
data/CLAUDE.md CHANGED
@@ -9,7 +9,7 @@ The primary gem for the LegionIO framework. An extensible async job engine for s
9
9
 
10
10
  **GitHub**: https://github.com/LegionIO/LegionIO
11
11
  **Gem**: `legionio`
12
- **Version**: 1.6.0
12
+ **Version**: 1.7.18
13
13
  **License**: Apache-2.0
14
14
  **Docker**: `legionio/legion`
15
15
  **Ruby**: >= 3.4
@@ -51,14 +51,14 @@ Legion.start
51
51
  ├── 10. setup_gaia (legion-gaia, cognitive coordination layer, optional)
52
52
  ├── 11. setup_telemetry (OpenTelemetry, optional)
53
53
  ├── 12. setup_supervision (process supervision)
54
- ├── 13. load_extensions (two-phase parallel: require+autobuild on FixedThreadPool, then hook_all_actors)
54
+ ├── 13. load_extensions (multi-phase: phase 0 (identity providers) loads and hooks actors first, then phase 1 (everything else))
55
55
  ├── 14. Legion::Crypt.cs (distribute cluster secret)
56
56
  └── 15. setup_api (start Sinatra/Puma on port 4567)
57
57
  ```
58
58
 
59
59
  Each phase calls `Legion::Readiness.mark_ready(:component)`. All phases are individually toggleable via `Service.new(transport: false, ...)`.
60
60
 
61
- Extension loading is two-phase and parallel: all extensions are `require`d and `autobuild` runs concurrently on a `Concurrent::FixedThreadPool(min(count, extensions.parallel_pool_size))`, collecting actors into a thread-safe `Concurrent::Array` of `@pending_actors`. Pool size defaults to 24, configurable via `Legion::Settings[:extensions][:parallel_pool_size]`. After all extensions are loaded, `hook_all_actors` starts AMQP subscriptions, timers, and other actor types sequentially. This prevents race conditions where early extensions start ticking while later ones haven't loaded yet. Thread safety relies on ThreadLocal AMQP channels, per-extension Settings keys, and sequential post-processing of Catalog transitions and Registry writes.
61
+ Extension loading is multi-phase and parallel: `hook_extensions` calls `group_by_phase` to partition discovered extensions by phase number (from the category registry), then iterates phases sequentially. Phase 0 contains identity providers (`lex-identity-*` gems, category `:identity`, tier 0); phase 1 contains all other extensions. Within each phase, extensions are `require`d and `autobuild` runs concurrently on a `Concurrent::FixedThreadPool(min(count, extensions.parallel_pool_size))`, collecting actors into a thread-safe `Concurrent::Array` of `@pending_actors`. Pool size defaults to 24, configurable via `Legion::Settings[:extensions][:parallel_pool_size]`. After each phase's extensions are loaded, `hook_phase_actors` starts AMQP subscriptions, timers, and other actor types for that phase sequentially ensuring identity providers are fully running before any other extension boots. Catalog transitions (`transition(:running)` and `flush_persisted_transitions`) happen after all phases complete. Thread safety relies on ThreadLocal AMQP channels, per-extension Settings keys, and sequential post-processing of Catalog transitions and Registry writes.
62
62
 
63
63
  ### Reload Sequence
64
64
 
@@ -85,7 +85,7 @@ Legion (lib/legion.rb)
85
85
  │ # Ingress.run(payload:, runner_class:, function:, source:)
86
86
  │ # Ingress.normalize returns message hash without executing
87
87
  ├── Extensions # LEX discovery, loading, and lifecycle management
88
- │ ├── Core # Mixin: data_required?, cache_required?, crypt_required?, etc.
88
+ │ ├── Core # Mixin: data_required?, cache_required?, crypt_required?, mcp_tools?, mcp_tools_deferred?, etc.
89
89
  │ ├── Actors/ # Actor execution modes
90
90
  │ │ ├── Base # Base actor class
91
91
  │ │ ├── Every # Run at interval (timer)
@@ -96,7 +96,7 @@ Legion (lib/legion.rb)
96
96
  │ │ └── Nothing # No-op actor
97
97
  │ ├── Builders/ # Build actors and runners from LEX definitions
98
98
  │ │ ├── Actors # Build actors from extension definitions
99
- │ │ ├── Runners # Build runners from extension definitions (stores runner_module ref)
99
+ │ │ ├── Runners # Build runners from extension definitions; exposes `runner_modules` accessor for Discovery
100
100
  │ │ ├── Helpers # Builder utilities
101
101
  │ │ ├── Hooks # Webhook hook system builder
102
102
  │ │ └── Routes # Auto-route builder: introspects runners, registers POST /api/extensions/* routes
@@ -149,7 +149,17 @@ Legion (lib/legion.rb)
149
149
  │ # Populated by Builders::Routes during autobuild via LexDispatch
150
150
 
151
151
  ├── MCP (legion-mcp gem) # Extracted to standalone gem — see legion-mcp/CLAUDE.md
152
- │ └── (58 tools, 2 resources, TierRouter, PatternStore, ContextGuard, Observer, EmbeddingIndex)
152
+ │ └── (tools, 2 resources, TierRouter, PatternStore, ContextGuard, Observer, EmbeddingIndex)
153
+
154
+ ├── Tools # Canonical tool layer — replaces Extensions::Capability and Catalog::Registry
155
+ │ ├── Base # Base class for all framework tools (Do, Status, Config are built-in statics)
156
+ │ ├── Registry # always/deferred classification for all tools; replaces Catalog::Registry
157
+ │ │ # Extensions declare tools via `mcp_tools?` / `mcp_tools_deferred?` DSL on Core
158
+ │ ├── Discovery # Auto-discovers tools from extension runner modules at boot
159
+ │ │ # `runner_modules` accessor on Builders::Runners feeds Discovery
160
+ │ │ # `loaded_extension_modules` on Extensions exposes the full set
161
+ │ └── EmbeddingCache # 5-tier persistent embedding cache:
162
+ │ # L0 in-memory hash → L1 Cache::Local → L2 Cache → L3 Data::Local → L4 Data
153
163
 
154
164
  ├── DigitalWorker # Digital worker platform (AI-as-labor governance)
155
165
  │ ├── Lifecycle # Worker state machine (active/paused/retired/terminated)
@@ -248,6 +258,16 @@ Legion (lib/legion.rb)
248
258
 
249
259
  `Legion::Extensions.find_extensions` discovers lex-* gems via `Bundler.load.specs` (when running under Bundler) or falls back to `Gem::Specification.all_names`. It also processes `Legion::Settings[:extensions]` for explicitly configured extensions, attempting `Gem.install` for missing ones if `auto_install` is enabled.
250
260
 
261
+ **Category registry**: Extensions are classified by `categorize_and_order` using `default_category_registry`. Each category has a `type` (`:list` or `:prefix`), `tier` (load order within a phase), and `phase`:
262
+
263
+ | Category | Type | Tier | Phase | Matches |
264
+ |----------|------|------|-------|---------|
265
+ | `identity` | prefix | 0 | 0 | `lex-identity-*` gems |
266
+ | `core` | list | 1 | 1 | explicitly listed core extensions |
267
+ | `ai` | list | 2 | 1 | explicitly listed AI provider extensions |
268
+ | `gaia` | list | 3 | 1 | explicitly listed GAIA extensions |
269
+ | `agentic` | prefix | 4 | 1 | `lex-agentic-*` gems |
270
+
251
271
  **Role-based filtering**: After discovery, `apply_role_filter` prunes extensions based on `Legion::Settings[:role][:profile]`:
252
272
 
253
273
  | Profile | What loads |
@@ -479,7 +499,7 @@ legion
479
499
 
480
500
  ### MCP Design
481
501
 
482
- Extracted to the `legion-mcp` gem (v0.5.9). See `legion-mcp/CLAUDE.md` for full architecture.
502
+ Extracted to the `legion-mcp` gem (v0.7.3). See `legion-mcp/CLAUDE.md` for full architecture.
483
503
 
484
504
  - `Legion::MCP.server` is memoized singleton — call `Legion::MCP.reset!` in tests
485
505
  - Tool naming: `legion.snake_case_name` (dot namespace, not slash)
@@ -571,7 +591,7 @@ rack-test, rake, rspec, rubocop, rubocop-rspec, simplecov
571
591
  | `lib/legion/readiness.rb` | Component readiness tracking (COMPONENTS constant, `ready?`, `to_h`) |
572
592
  | `lib/legion/events.rb` | In-process pub/sub: `on`, `emit`, `once`, `off`, wildcard `*` |
573
593
  | `lib/legion/ingress.rb` | Universal runner invocation: `normalize`, `run` |
574
- | `lib/legion/extensions.rb` | LEX discovery, loading, actor hooking, shutdown |
594
+ | `lib/legion/extensions.rb` | LEX discovery, loading, actor hooking, shutdown; exposes `loaded_extension_modules` for Tools::Discovery |
575
595
  | `lib/legion/extensions/core.rb` | Extension mixin (requirement flags, autobuild) |
576
596
  | `lib/legion/extensions/actors/` | Actor types: base, every, loop, once, poll, subscription, nothing, defaults |
577
597
  | `lib/legion/extensions/builders/` | Build actors, runners, helpers, hooks, routes from definitions |
@@ -586,7 +606,12 @@ rack-test, rake, rspec, rubocop, rubocop-rspec, simplecov
586
606
  | `lib/legion/isolation.rb` | Process isolation for untrusted extension execution |
587
607
  | `lib/legion/sandbox.rb` | Sandboxed execution environment for extensions |
588
608
  | `lib/legion/context.rb` | Thread-local execution context (request tracing, tenant) |
589
- | `lib/legion/catalog.rb` | Extension catalog: registry of available extensions with metadata |
609
+ | `lib/legion/catalog.rb` | Extension catalog: registry of available extensions with metadata (Catalog::Registry removed — replaced by Tools::Registry) |
610
+ | `lib/legion/tools.rb` | Tools module entry point |
611
+ | `lib/legion/tools/base.rb` | Tools::Base — canonical base class for all tools |
612
+ | `lib/legion/tools/registry.rb` | Tools::Registry — always/deferred classification, replaces Catalog::Registry |
613
+ | `lib/legion/tools/discovery.rb` | Tools::Discovery — auto-discovers tools from extension runner_modules at boot |
614
+ | `lib/legion/tools/embedding_cache.rb` | Tools::EmbeddingCache — 5-tier persistent embedding cache (L0–L4) |
590
615
  | `lib/legion/registry.rb` | Extension registry with security scanning |
591
616
  | `lib/legion/registry/security_scanner.rb` | Gem security scanner (CVE checks, signature verification) |
592
617
  | `lib/legion/webhooks.rb` | Webhook delivery system: HTTP POST with retry, HMAC signing |
@@ -158,7 +158,7 @@ module Legion
158
158
  end
159
159
  return nil unless matched
160
160
 
161
- matched.tool_name.split('.').last
161
+ matched.tool_name.split(/[-.]/).last
162
162
  end
163
163
 
164
164
  def build_runner_class(extension, runner)
@@ -21,12 +21,21 @@ module Legion
21
21
  @local_tasks = []
22
22
  @actors = []
23
23
  @running_instances = Concurrent::Array.new
24
- @pending_actors = Concurrent::Array.new
24
+ @loaded_extensions = []
25
25
 
26
26
  find_extensions
27
- load_extensions
27
+
28
+ phases = group_by_phase
29
+ phases.each do |phase_num, entries|
30
+ @pending_actors = Concurrent::Array.new
31
+ load_phase_extensions(phase_num, entries)
32
+ hook_phase_actors(phase_num)
33
+ end
34
+
35
+ @loaded_extensions&.each { |name| Catalog.transition(name, :running) }
36
+ Catalog.flush_persisted_transitions
37
+
28
38
  load_yaml_agents
29
- hook_all_actors
30
39
  end
31
40
 
32
41
  attr_reader :local_tasks
@@ -107,11 +116,8 @@ module Legion
107
116
  Legion::Logging.warn 'All actors paused' if defined?(Legion::Logging)
108
117
  end
109
118
 
110
- def load_extensions
111
- @extensions ||= []
112
- @loaded_extensions ||= []
113
-
114
- eligible = @extensions.filter_map do |entry|
119
+ def load_phase_extensions(phase_num, entries)
120
+ eligible = entries.filter_map do |entry|
115
121
  gem_name = entry[:gem_name]
116
122
  ext_name = entry[:require_path].split('/').last
117
123
 
@@ -130,15 +136,35 @@ module Legion
130
136
  load_extensions_parallel(eligible)
131
137
 
132
138
  Legion::Logging.info(
133
- "#{@extensions.count} extensions loaded with " \
134
- "subscription:#{@subscription_tasks.count}," \
139
+ "Phase #{phase_num}: #{eligible.count} extensions loaded " \
140
+ "(subscription:#{@subscription_tasks.count}," \
135
141
  "every:#{@timer_tasks.count}," \
136
142
  "poll:#{@poll_tasks.count}," \
137
143
  "once:#{@once_tasks.count}," \
138
- "loop:#{@loop_tasks.count}"
144
+ "loop:#{@loop_tasks.count})"
139
145
  )
140
146
  end
141
147
 
148
+ def hook_phase_actors(phase_num)
149
+ return if @pending_actors.nil? || @pending_actors.empty?
150
+
151
+ Legion::Logging.info "Phase #{phase_num}: hooking #{@pending_actors.size} deferred actors"
152
+
153
+ groups = group_pending_actors
154
+
155
+ %i[once poll every loop].each do |type|
156
+ next if groups[type].empty?
157
+
158
+ groups[type].each { |actor| hook_actor(**actor) }
159
+ end
160
+
161
+ hook_subscription_actors_pooled(groups[:subscription]) unless groups[:subscription].empty?
162
+
163
+ dispatch_local_actors(@local_tasks) unless @local_tasks.empty?
164
+
165
+ @pending_actors.clear
166
+ end
167
+
142
168
  def load_extensions_parallel(eligible)
143
169
  return if eligible.empty?
144
170
 
@@ -273,38 +299,6 @@ module Legion
273
299
  false
274
300
  end
275
301
 
276
- def hook_all_actors
277
- return if @pending_actors.nil? || @pending_actors.empty?
278
-
279
- Legion::Logging.info "Hooking #{@pending_actors.size} deferred actors"
280
-
281
- groups = group_pending_actors
282
-
283
- %i[once poll every loop].each do |type|
284
- next if groups[type].empty?
285
-
286
- Legion::Logging.info "Starting #{type} actors (#{groups[type].size})"
287
- groups[type].each { |actor| hook_actor(**actor) }
288
- end
289
- unless groups[:subscription].empty?
290
- Legion::Logging.info "Starting subscription actors (#{groups[:subscription].size})"
291
- hook_subscription_actors_pooled(groups[:subscription])
292
- end
293
- dispatch_local_actors(@local_tasks) unless @local_tasks.empty?
294
-
295
- @pending_actors.clear
296
- Legion::Logging.info(
297
- "Actors hooked: subscription:#{@subscription_tasks.count}," \
298
- "every:#{@timer_tasks.count}," \
299
- "poll:#{@poll_tasks.count}," \
300
- "once:#{@once_tasks.count}," \
301
- "loop:#{@loop_tasks.count}," \
302
- "local:#{@local_tasks.count}"
303
- )
304
- @loaded_extensions&.each { |name| Catalog.transition(name, :running) }
305
- Catalog.flush_persisted_transitions
306
- end
307
-
308
302
  ACTOR_TYPE_MAP = {
309
303
  Once: :once,
310
304
  Poll: :poll,
@@ -313,6 +307,17 @@ module Legion
313
307
  Subscription: :subscription
314
308
  }.freeze
315
309
 
310
+ def group_by_phase
311
+ settings_cats = ::Legion::Settings.dig(:extensions, :categories) || {}
312
+ categories = default_category_registry.merge(settings_cats)
313
+ default_phase = 1
314
+
315
+ @extensions.group_by do |entry|
316
+ cat = entry[:category]
317
+ categories.dig(cat, :phase) || default_phase
318
+ end.sort_by(&:first)
319
+ end
320
+
316
321
  def group_pending_actors
317
322
  groups = { once: [], poll: [], every: [], loop: [], subscription: [] }
318
323
  @pending_actors.each do |actor|
@@ -661,9 +666,10 @@ module Legion
661
666
  ext_settings = ::Legion::Settings[:extensions] || {}
662
667
  categories = ext_settings[:categories] || default_category_registry
663
668
  lists = {
664
- core: Array(ext_settings[:core]),
665
- ai: Array(ext_settings[:ai]),
666
- gaia: Array(ext_settings[:gaia])
669
+ identity: Array(ext_settings[:identity]),
670
+ core: Array(ext_settings[:core]),
671
+ ai: Array(ext_settings[:ai]),
672
+ gaia: Array(ext_settings[:gaia])
667
673
  }
668
674
  ctx = {
669
675
  blocked: Array(ext_settings[:blocked]),
@@ -696,7 +702,7 @@ module Legion
696
702
  Legion::Logging.debug "Extensions#check_reserved_words failed to read reserved_prefixes: #{e.message}" if defined?(Legion::Logging)
697
703
  []
698
704
  end
699
- reserved_prefixes = configured_prefixes.empty? ? %w[core ai agentic gaia] : configured_prefixes
705
+ reserved_prefixes = configured_prefixes.empty? ? %w[core ai agentic gaia identity] : configured_prefixes
700
706
 
701
707
  configured_words = begin
702
708
  Array(::Legion::Settings.dig(:extensions, :reserved_words))
@@ -881,10 +887,11 @@ module Legion
881
887
 
882
888
  def default_category_registry
883
889
  {
884
- core: { type: :list, tier: 1 },
885
- ai: { type: :list, tier: 2 },
886
- gaia: { type: :list, tier: 3 },
887
- agentic: { type: :prefix, tier: 4 }
890
+ identity: { type: :prefix, tier: 0, phase: 0 },
891
+ core: { type: :list, tier: 1, phase: 1 },
892
+ ai: { type: :list, tier: 2, phase: 1 },
893
+ gaia: { type: :list, tier: 3, phase: 1 },
894
+ agentic: { type: :prefix, tier: 4, phase: 1 }
888
895
  }
889
896
  end
890
897
 
@@ -6,6 +6,7 @@ module Legion
6
6
  tool_name 'legion.get_config'
7
7
  description 'Get Legion configuration (sensitive values are redacted).'
8
8
  input_schema(
9
+ type: 'object',
9
10
  properties: {
10
11
  section: { type: 'string', description: 'Specific config section (e.g., "transport", "data")' }
11
12
  }
@@ -3,6 +3,13 @@
3
3
  module Legion
4
4
  module Tools
5
5
  module Discovery
6
+ # Extension/runner pairs that should always be loaded (not deferred)
7
+ # nil means all runners for that extension; array means specific runners only
8
+ ALWAYS_LOADED = {
9
+ 'apollo' => ['knowledge'],
10
+ 'eval' => ['evaluation']
11
+ }.freeze
12
+
6
13
  class << self
7
14
  def log
8
15
  Legion::Logging.respond_to?(:logger) ? Legion::Logging.logger : nil
@@ -15,11 +22,19 @@ module Legion
15
22
  def discover_and_register
16
23
  return unless defined?(Legion::Extensions)
17
24
 
18
- loaded_extensions.each do |ext|
25
+ exts = loaded_extensions
26
+ log&.info("[Tools::Discovery] scanning #{exts.size} extensions")
27
+
28
+ exts.each do |ext|
19
29
  discover_runners(ext)
20
30
  rescue StandardError => e
21
31
  handle_exception(e, level: :warn, handled: true, operation: :discovery_process_extension)
22
32
  end
33
+
34
+ log&.info(
35
+ "[Tools::Discovery] done: always=#{Registry.tools.size} " \
36
+ "deferred=#{Registry.deferred_tools.size}"
37
+ )
23
38
  end
24
39
 
25
40
  private
@@ -58,8 +73,6 @@ module Legion
58
73
  end
59
74
  end
60
75
 
61
- # Build a functions hash from class_methods when settings[:functions] is not populated.
62
- # The builders/runners.rb populates class_methods but not settings[:functions] by default.
63
76
  def synthesize_functions(ext, runner_mod)
64
77
  return {} unless ext.respond_to?(:runners) && ext.runners.is_a?(Hash)
65
78
 
@@ -87,7 +100,6 @@ module Legion
87
100
  Legion::Tools::Registry.register(tool_class)
88
101
  end
89
102
 
90
- # Hierarchical: runner overrides extension
91
103
  def resolve_mcp_tools_enabled(ext, runner_mod)
92
104
  return runner_mod.mcp_tools? if runner_mod.respond_to?(:mcp_tools?)
93
105
 
@@ -95,6 +107,13 @@ module Legion
95
107
  end
96
108
 
97
109
  def resolve_deferred(ext, runner_mod)
110
+ ext_name = derive_extension_name(ext)
111
+ runner_name = derive_runner_snake(runner_mod)
112
+ if ALWAYS_LOADED.key?(ext_name)
113
+ runners = ALWAYS_LOADED[ext_name]
114
+ return false if runners.nil? || runners.include?(runner_name)
115
+ end
116
+
98
117
  return runner_mod.mcp_tools_deferred? if runner_mod.respond_to?(:mcp_tools_deferred?)
99
118
 
100
119
  ext.respond_to?(:mcp_tools_deferred?) ? ext.mcp_tools_deferred? : true
@@ -128,9 +147,9 @@ module Legion
128
147
  ext_name = derive_extension_name(ext)
129
148
  runner_snake = derive_runner_snake(runner_mod)
130
149
  {
131
- tool_name: defn&.dig(:mcp_prefix) || "legion.#{ext_name}.#{runner_snake}.#{func_name}",
150
+ tool_name: defn&.dig(:mcp_prefix) || "legion-#{ext_name}-#{runner_snake}-#{func_name}",
132
151
  description: meta[:desc] || defn&.dig(:desc) || "#{ext_name}##{func_name}",
133
- input_schema: meta[:options] || { properties: {} },
152
+ input_schema: normalize_schema(meta[:options]),
134
153
  mcp_category: defn&.dig(:mcp_category),
135
154
  mcp_tier: defn&.dig(:mcp_tier),
136
155
  deferred: deferred,
@@ -173,6 +192,14 @@ module Legion
173
192
  last.gsub(/([A-Z])/, '_\1').sub(/^_/, '').downcase
174
193
  end
175
194
 
195
+ def normalize_schema(schema)
196
+ schema = { properties: {} } if schema.nil? || schema.empty?
197
+ schema = schema.dup
198
+ schema[:type] ||= 'object'
199
+ schema[:properties] ||= {}
200
+ schema
201
+ end
202
+
176
203
  def derive_extension_name(ext)
177
204
  if ext.respond_to?(:lex_name)
178
205
  ext.lex_name.delete_prefix('lex-').tr('-', '_')
@@ -9,6 +9,7 @@ module Legion
9
9
  description 'Execute a Legion action by describing what you want to do in natural language. ' \
10
10
  'Routes to the best matching tool automatically.'
11
11
  input_schema(
12
+ type: 'object',
12
13
  properties: {
13
14
  intent: {
14
15
  type: 'string',
@@ -51,7 +51,10 @@ module Legion
51
51
  end
52
52
  end
53
53
 
54
- # Catalog queries - replaces Catalog::Registry
54
+ def always_loaded_names
55
+ tools.map(&:tool_name)
56
+ end
57
+
55
58
  def for_extension(ext_name)
56
59
  all_tools.select { |t| t.respond_to?(:extension) && t.extension == ext_name }
57
60
  end
@@ -5,7 +5,7 @@ module Legion
5
5
  class Status < Base
6
6
  tool_name 'legion.get_status'
7
7
  description 'Get Legion service health status and component info.'
8
- input_schema(properties: {})
8
+ input_schema(type: 'object', properties: {})
9
9
 
10
10
  class << self
11
11
  include Legion::Logging::Helper
data/lib/legion/tools.rb CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Tools
5
- # Static tool classes accumulate here at require time for reload safety
6
5
  @tool_classes = []
7
6
  @mutex = Mutex.new
8
7
 
@@ -17,7 +16,6 @@ module Legion
17
16
  end
18
17
  end
19
18
 
20
- # Called by Service#register_core_tools on boot AND reload
21
19
  def register_all
22
20
  @mutex.synchronize { @tool_classes.dup }.each do |klass|
23
21
  Legion::Tools::Registry.register(klass)
@@ -32,7 +30,6 @@ require_relative 'tools/base'
32
30
  require_relative 'tools/discovery'
33
31
  require_relative 'tools/embedding_cache'
34
32
 
35
- # Static tools with custom orchestration logic
36
33
  Dir[File.join(__dir__, 'tools', '*.rb')].each do |f|
37
34
  require f unless f.end_with?('/base.rb', '/registry.rb', '/discovery.rb', '/embedding_cache.rb')
38
35
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.7.17'
4
+ VERSION = '1.7.19'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legionio
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.17
4
+ version: 1.7.19
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity