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 +4 -4
- data/CHANGELOG.md +26 -0
- data/CLAUDE.md +34 -9
- data/lib/legion/cli/do_command.rb +1 -1
- data/lib/legion/extensions.rb +58 -51
- data/lib/legion/tools/config.rb +1 -0
- data/lib/legion/tools/discovery.rb +33 -6
- data/lib/legion/tools/do.rb +1 -0
- data/lib/legion/tools/registry.rb +4 -1
- data/lib/legion/tools/status.rb +1 -1
- data/lib/legion/tools.rb +0 -3
- data/lib/legion/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c2114c2b32ae4dbc1650bef546391f6abb1499a0b9d96d577739503c47caba79
|
|
4
|
+
data.tar.gz: 1e95267b23de0b635ffb32c9f745f58fe0bac63aa7d806a6581e3557b9b1aad6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
│ └── (
|
|
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.
|
|
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 |
|
data/lib/legion/extensions.rb
CHANGED
|
@@ -21,12 +21,21 @@ module Legion
|
|
|
21
21
|
@local_tasks = []
|
|
22
22
|
@actors = []
|
|
23
23
|
@running_instances = Concurrent::Array.new
|
|
24
|
-
@
|
|
24
|
+
@loaded_extensions = []
|
|
25
25
|
|
|
26
26
|
find_extensions
|
|
27
|
-
|
|
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
|
|
111
|
-
|
|
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
|
-
"#{
|
|
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
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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
|
|
data/lib/legion/tools/config.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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]
|
|
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('-', '_')
|
data/lib/legion/tools/do.rb
CHANGED
|
@@ -51,7 +51,10 @@ module Legion
|
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
|
|
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
|
data/lib/legion/tools/status.rb
CHANGED
|
@@ -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
|
data/lib/legion/version.rb
CHANGED