legionio 1.7.37 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2bafa8899bbfa7e80970cec50a67e3989fc259cc426ebbbf7bc531194bd8b0d8
4
- data.tar.gz: 4e768d2523b3a9b962f8d1119061fe25d71c8e101acb071cef793b40695ebb8c
3
+ metadata.gz: bce17019cfaa062e933bb282732b8ac7261a52d94dbbe1ec9338b293cc11ed7f
4
+ data.tar.gz: a1740ed3e87db1177227e0644489b245096e5870e01c36069a7c7cb2db0efcef
5
5
  SHA512:
6
- metadata.gz: c20eb127ef11c1f60ad06c42619354a91072a3c79ff103189454c6f8080e60fa1e1b5ad9bdcb77703ccfd3308079117aadad6c7f063f59c5255a01b635d2c3dc
7
- data.tar.gz: 0bcaa14c6e8c8a3fd0c9b5f47d2cb64abba2cee271cb09eadf812a460f0bfe5069147bb497b205e910fffa71da98702c3889805ef8d18f09cd7d7d8714020c38
6
+ metadata.gz: e4cf3b2db7dc3bfca5ce8deb357bc6491e10cd3eea398d80a48993ef8d982211f75d9afb4fa59a112f4087c4e6660a98050d636631a6802fa804ebcbe0c0e9f7
7
+ data.tar.gz: 5b66c3b1010022bcfca3ba5c76f758a6878572930cda2e8ccde1f90a934c312e30e58c48ea31f6c29e0468eaef60b0c1d994e79295e53243b5f2e21f05e2181b
data/CHANGELOG.md CHANGED
@@ -8,6 +8,17 @@
8
8
  - Trigger word tool injection: extensions and runners declare trigger words that auto-promote deferred tools when detected in LLM messages
9
9
  - `Legion::Tools::TriggerIndex` — Concurrent::Map-backed reverse index for O(1) trigger word lookup
10
10
  - `trigger_words` DSL on Extensions::Core, runner modules, and Tools::Base
11
+ - also fixed the stupid thor rspec issue
12
+
13
+ ## [1.8.0] - 2026-04-12
14
+
15
+ ### Added
16
+ - `Legion::Extensions::Builder::Skills` — parallel to `Builders::Runners`, discovers and registers `lex-skill-*` gems into `Legion::LLM::Skills::Registry` at boot
17
+ - `Legion::Extensions::Core` — `skills_required?` guard; extensions declaring this flag are skipped when legion-llm is not loaded
18
+ - `Legion::Chat::Skills` rewritten — delegates to `Legion::LLM::Skills::Registry` instead of YAML file discovery; `discover` returns an Array of skill objects
19
+ - `Legion::API::Skills` — REST endpoints: `GET /api/skills`, `GET /api/skills/:namespace/:name`, `POST /api/skills/invoke`, `DELETE /api/skills/active/:conversation_id`
20
+ - `Legion::CLI::SkillCommand` rewritten — delegates to daemon API instead of local YAML parsing; `list`, `show`, `run` subcommands
21
+ - `Legion::Extensions::Builder::Skills` wired into `Extensions::Core#autobuild` after `Builders::Runners`
11
22
 
12
23
  ## [1.7.36] - 2026-04-09
13
24
 
@@ -15,8 +15,8 @@ module Legion
15
15
  Legion::LLM.started?
16
16
 
17
17
  halt 503, { 'Content-Type' => 'application/json' },
18
- Legion::JSON.dump({ error: { code: 'llm_unavailable',
19
- message: 'LLM subsystem is not available' } })
18
+ Legion::JSON.generate({ error: { code: 'llm_unavailable',
19
+ message: 'LLM subsystem is not available' } })
20
20
  end
21
21
 
22
22
  define_method(:cache_available?) do
@@ -241,7 +241,7 @@ module Legion
241
241
 
242
242
  unless messages.is_a?(Array)
243
243
  halt 400, { 'Content-Type' => 'application/json' },
244
- Legion::JSON.dump({ error: { code: 'invalid_messages', message: 'messages must be an array' } })
244
+ Legion::JSON.generate({ error: { code: 'invalid_messages', message: 'messages must be an array' } })
245
245
  end
246
246
 
247
247
  caller_identity = env['legion.tenant_id'] || 'api:inference'
@@ -287,6 +287,7 @@ module Legion
287
287
  { requested_by: { identity: caller_identity, type: :user, credential: :api } }
288
288
  end
289
289
 
290
+ caller_metadata = body[:metadata].is_a?(Hash) ? body[:metadata] : {}
290
291
  req = Legion::LLM::Pipeline::Request.build(
291
292
  messages: messages,
292
293
  system: body[:system],
@@ -294,7 +295,7 @@ module Legion
294
295
  tools: tool_classes,
295
296
  caller: caller_ctx,
296
297
  conversation_id: body[:conversation_id],
297
- metadata: { requested_tools: requested_tools },
298
+ metadata: caller_metadata.merge(requested_tools: requested_tools),
298
299
  stream: streaming,
299
300
  cache: { strategy: :default, cacheable: true }
300
301
  )
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ class API < Sinatra::Base
7
+ module Routes
8
+ module Skills
9
+ def self.registered(app)
10
+ app.helpers do
11
+ define_method(:skills_registry_available?) do
12
+ defined?(Legion::LLM::Skills::Registry)
13
+ end
14
+
15
+ define_method(:skill_descriptor) do |skill|
16
+ {
17
+ name: skill.skill_name,
18
+ namespace: skill.namespace,
19
+ description: skill.description,
20
+ trigger: skill.trigger,
21
+ follows: skill.follows_skill
22
+ }
23
+ end
24
+ end
25
+
26
+ register_list(app)
27
+ register_show(app)
28
+ register_invoke(app)
29
+ register_cancel(app)
30
+ end
31
+
32
+ def self.register_list(app)
33
+ app.get '/api/skills' do
34
+ return json_error('skills_unavailable', 'Skills unavailable', status_code: 503) unless skills_registry_available?
35
+
36
+ skills = Legion::LLM::Skills::Registry.all.map { |s| skill_descriptor(s) }
37
+ json_response(skills)
38
+ end
39
+ end
40
+
41
+ def self.register_show(app)
42
+ app.get '/api/skills/:namespace/:name' do
43
+ return json_error('skills_unavailable', 'Skills unavailable', status_code: 503) unless skills_registry_available?
44
+
45
+ key = "#{params[:namespace]}:#{params[:name]}"
46
+ skill = Legion::LLM::Skills::Registry.find(key)
47
+ return json_error('not_found', "Skill #{key} not found", status_code: 404) unless skill
48
+
49
+ json_response(skill_descriptor(skill).merge(steps: skill.steps))
50
+ end
51
+ end
52
+
53
+ def self.register_invoke(app)
54
+ app.post '/api/skills/invoke' do
55
+ return json_error('skills_unavailable', 'Skills unavailable', status_code: 503) unless skills_registry_available?
56
+
57
+ body = parse_request_body
58
+ skill_name = body[:skill_name]
59
+ return json_error('unprocessable', 'skill_name required', status_code: 422) if skill_name.nil? || skill_name.empty?
60
+
61
+ skill_class = Legion::LLM::Skills::Registry.find(skill_name)
62
+ return json_error('not_found', "Skill #{skill_name} not found", status_code: 404) unless skill_class
63
+
64
+ conv_id = body[:conversation_id] || "conv_#{SecureRandom.hex(8)}"
65
+ begin
66
+ Legion::LLM::ConversationStore.set_skill_state(conv_id, skill_key: skill_name, resume_at: 0)
67
+ req = Legion::LLM::Pipeline::Request.build(
68
+ messages: [{ role: :user, content: body[:initial_message] || 'start skill' }],
69
+ conversation_id: conv_id,
70
+ metadata: (body[:metadata].is_a?(Hash) ? body[:metadata] : {}).merge(skill_invoke: true),
71
+ stream: false
72
+ )
73
+ result = Legion::LLM::Pipeline::Executor.new(req).call
74
+ json_response({ conversation_id: conv_id, content: result.message[:content],
75
+ skill_name: skill_name })
76
+ rescue StandardError => e
77
+ Legion::LLM::ConversationStore.clear_skill_state(conv_id)
78
+ json_error('internal_error', e.message, status_code: 500)
79
+ end
80
+ end
81
+ end
82
+
83
+ def self.register_cancel(app)
84
+ app.delete '/api/skills/active/:conversation_id' do
85
+ conv_id = params[:conversation_id]
86
+ if defined?(Legion::LLM::ConversationStore)
87
+ state = Legion::LLM::ConversationStore.cancel_skill!(conv_id)
88
+ if state && defined?(Legion::Events)
89
+ Legion::Events.emit('skill.cancelled', conversation_id: conv_id,
90
+ skill_name: state[:skill_key])
91
+ end
92
+ end
93
+ status 204
94
+ end
95
+ end
96
+
97
+ private_class_method :register_list, :register_show, :register_invoke, :register_cancel
98
+ end
99
+ end
100
+ end
101
+ end
data/lib/legion/api.rb CHANGED
@@ -35,6 +35,7 @@ require_relative 'api/capacity'
35
35
  require_relative 'api/audit'
36
36
  require_relative 'api/metrics'
37
37
  require_relative 'api/llm'
38
+ require_relative 'api/skills'
38
39
  require_relative 'api/catalog'
39
40
  require_relative 'api/org_chart'
40
41
  require_relative 'api/workflow'
@@ -197,6 +198,7 @@ module Legion
197
198
  register Routes::Audit
198
199
  register Routes::Metrics
199
200
  mount_library_routes('llm', Routes::Llm, 'Legion::LLM::Routes')
201
+ register Routes::Skills
200
202
  register Routes::ExtensionCatalog
201
203
  register Routes::OrgChart
202
204
  register Routes::Governance
@@ -1,123 +1,62 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'yaml'
4
-
5
3
  module Legion
6
4
  module Chat
7
5
  module Skills
8
- SKILL_DIRS = ['.legion/skills', '~/.legionio/skills'].freeze
9
-
10
6
  class << self
11
7
  def discover
12
- SKILL_DIRS.flat_map do |dir|
13
- expanded = File.expand_path(dir)
14
- next [] unless Dir.exist?(expanded)
8
+ return file_discover unless llm_skills_available?
15
9
 
16
- md_skills = Dir.glob(File.join(expanded, '*.md')).filter_map { |f| parse(f) }
17
- rb_skills = Dir.glob(File.join(expanded, '*.rb')).filter_map { |f| parse_rb(f) }
18
- md_skills + rb_skills
19
- end
10
+ Legion::LLM::Skills::Registry.all.map { |s| registry_descriptor(s) }
20
11
  end
21
12
 
22
13
  def find(name)
23
- discover.find { |s| s[:name] == name.to_s }
24
- end
25
-
26
- def parse(path)
27
- content = File.read(path)
28
- return nil unless content.start_with?('---')
29
-
30
- parts = content.split(/^---\s*$/, 3)
31
- return nil if parts.size < 3
14
+ return file_find(name) unless llm_skills_available?
32
15
 
33
- frontmatter = YAML.safe_load(parts[1], permitted_classes: [Symbol])
34
- body = parts[2]&.strip
35
-
36
- {
37
- name: frontmatter['name'] || File.basename(path, '.md'),
38
- description: frontmatter['description'] || '',
39
- type: :prompt,
40
- model: frontmatter['model'],
41
- tools: Array(frontmatter['tools']),
42
- prompt: body,
43
- path: path
44
- }
45
- rescue StandardError => e
46
- Legion::Logging.warn "Skill parse error #{path}: #{e.message}" if defined?(Legion::Logging)
47
- nil
16
+ skill = Legion::LLM::Skills::Registry.find(name)
17
+ skill ? registry_descriptor(skill) : nil
48
18
  end
49
19
 
50
- def parse_rb(path)
51
- content = File.read(path)
20
+ # execute: REMOVED — all skill execution routes through the daemon API.
21
+ # `legion skill run` / `legion chat` are thin HTTP clients; no local LLM boot.
52
22
 
53
- name = File.basename(path, '.rb')
54
- description = content.match(/^\s*#\s*description:\s*(.+)$/i)&.captures&.first || ''
55
- model = content.match(/^\s*#\s*model:\s*(.+)$/i)&.captures&.first
23
+ private
56
24
 
57
- {
58
- name: name,
59
- description: description.strip,
60
- type: :ruby,
61
- model: model&.strip,
62
- tools: [],
63
- prompt: nil,
64
- path: path
65
- }
66
- rescue StandardError => e
67
- Legion::Logging.warn "Skill parse_rb error #{path}: #{e.message}" if defined?(Legion::Logging)
68
- nil
25
+ def llm_skills_available?
26
+ defined?(Legion::LLM::Skills) &&
27
+ Legion::LLM.respond_to?(:started?) &&
28
+ Legion::LLM.started?
69
29
  end
70
30
 
71
- def execute(skill, input: nil)
72
- case skill[:type]
73
- when :ruby
74
- execute_rb(skill, input: input)
75
- when :prompt
76
- execute_prompt(skill, input: input)
77
- else
78
- { success: false, error: "unknown skill type: #{skill[:type]}" }
79
- end
31
+ def registry_descriptor(skill)
32
+ { name: skill.skill_name, namespace: skill.namespace, prompt: nil,
33
+ description: skill.description, source: :registry }
80
34
  end
81
35
 
82
- private
83
-
84
- def execute_prompt(skill, input: nil)
85
- return { success: false, error: 'Legion::LLM not available' } unless defined?(Legion::LLM) && Legion::LLM.respond_to?(:chat_direct)
86
-
87
- prompt = skill[:prompt]
88
- prompt = "#{prompt}\n\nUser input: #{input}" if input
89
-
90
- session = Legion::LLM.chat_direct(model: skill[:model], provider: nil)
91
- response = session.ask(prompt)
92
- content = response.respond_to?(:content) ? response.content : response.to_s
93
-
94
- { success: true, output: content }
95
- rescue StandardError => e
96
- { success: false, error: e.message }
36
+ def file_discover
37
+ dirs = skill_directories
38
+ dirs.flat_map { |dir| ::Dir.glob(::File.join(dir, '*.{md,rb,yml,yaml}')) }
39
+ .map { |f| { name: ::File.basename(f, '.*'), path: f, source: :file } }
97
40
  end
98
41
 
99
- def execute_rb(skill, input: nil)
100
- begin
101
- real_path = File.realpath(skill[:path])
102
- rescue Errno::ENOENT
103
- return { success: false, error: "skill file not found: #{skill[:path]}" }
104
- end
105
- allowed = SKILL_DIRS.filter_map do |dir|
106
- expanded = File.expand_path(dir)
107
- File.realpath(expanded) if Dir.exist?(expanded)
108
- end
109
- unless allowed.any? { |dir| real_path.start_with?("#{dir}/") }
110
- return { success: false, error: "skill path outside allowed directories: #{real_path}" }
111
- end
42
+ def file_find(name)
43
+ dirs = skill_directories
44
+ dirs.each do |dir|
45
+ %w[.md .rb .yml .yaml].each do |ext|
46
+ path = ::File.join(dir, "#{name}#{ext}")
47
+ next unless ::File.exist?(path)
112
48
 
113
- mod = Module.new
114
- mod.module_eval(File.read(real_path), real_path)
115
- return { success: false, error: "#{skill[:name]}.rb must define a module-level `self.call` method" } unless mod.respond_to?(:call)
49
+ return { name: name, path: path, prompt: ::File.read(path), source: :file }
50
+ end
51
+ end
52
+ nil
53
+ end
116
54
 
117
- result = mod.call(input: input)
118
- { success: true, output: result }
119
- rescue StandardError => e
120
- { success: false, error: e.message }
55
+ def skill_directories
56
+ [
57
+ ::File.expand_path('.legion/skills'),
58
+ ::File.expand_path('~/.legionio/skills')
59
+ ].select { |d| ::File.directory?(d) }
121
60
  end
122
61
  end
123
62
  end
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'thor'
4
+ require 'net/http'
5
+ require 'json'
6
+ require 'uri'
4
7
 
5
8
  module Legion
6
9
  module CLI
@@ -9,45 +12,50 @@ module Legion
9
12
  true
10
13
  end
11
14
 
12
- desc 'list', 'List all discovered skills'
15
+ desc 'list', 'List all registered skills'
13
16
  def list
14
- require 'legion/chat/skills'
15
- skills = Legion::Chat::Skills.discover
17
+ response = daemon_get('/api/skills')
18
+ unless response.is_a?(::Net::HTTPSuccess)
19
+ say "Error fetching skills: #{response.code}", :red
20
+ exit 1
21
+ end
22
+
23
+ skills = ::JSON.parse(response.body, symbolize_names: true)[:data] || []
16
24
  if skills.empty?
17
- say 'No skills found. Create skills in .legion/skills/ or ~/.legionio/skills/'
25
+ say 'No skills registered. Start the daemon with legion-llm loaded.'
18
26
  return
19
27
  end
20
28
 
21
29
  skills.each do |s|
22
- type_label = s[:type] == :ruby ? '[rb]' : '[md]'
23
- say " /#{s[:name]} #{type_label} — #{s[:description]}", :green
24
- say " model: #{s[:model] || 'default'}, tools: #{s[:tools].empty? ? 'none' : s[:tools].join(', ')}"
30
+ say " #{s[:namespace]}:#{s[:name]} [#{s[:trigger]}] #{s[:description]}", :green
25
31
  end
26
32
  end
27
33
 
28
- desc 'show NAME', 'Display skill definition'
34
+ desc 'show NAMESPACE:NAME', 'Show skill details'
29
35
  def show(name)
30
- require 'legion/chat/skills'
31
- skill = Legion::Chat::Skills.find(name)
32
- if skill
33
- say "Name: #{skill[:name]}", :green
34
- say "Description: #{skill[:description]}"
35
- say "Model: #{skill[:model] || 'default'}"
36
- say "Tools: #{skill[:tools].empty? ? 'none' : skill[:tools].join(', ')}"
37
- say "Path: #{skill[:path]}"
38
- say "\n--- Prompt ---\n#{skill[:prompt]}"
39
- else
36
+ ns, nm = name.include?(':') ? name.split(':', 2) : ['default', name]
37
+ response = daemon_get("/api/skills/#{ns}/#{nm}")
38
+ unless response.is_a?(::Net::HTTPSuccess)
40
39
  say "Skill '#{name}' not found", :red
40
+ exit 1
41
41
  end
42
+
43
+ result = ::JSON.parse(response.body, symbolize_names: true)
44
+ data = result[:data] || {}
45
+ say "Name: #{data[:namespace]}:#{data[:name]}", :green
46
+ say "Description: #{data[:description]}"
47
+ say "Trigger: #{data[:trigger]}"
48
+ say "Steps: #{Array(data[:steps]).join(', ')}"
42
49
  end
43
50
 
44
51
  desc 'create NAME', 'Scaffold a new skill file'
45
52
  def create(name)
53
+ require 'fileutils'
46
54
  dir = '.legion/skills'
47
55
  FileUtils.mkdir_p(dir)
48
- path = File.join(dir, "#{name}.md")
56
+ path = ::File.join(dir, "#{name}.md")
49
57
 
50
- if File.exist?(path)
58
+ if ::File.exist?(path)
51
59
  say "Skill already exists: #{path}", :red
52
60
  return
53
61
  end
@@ -55,35 +63,49 @@ module Legion
55
63
  content = <<~SKILL
56
64
  ---
57
65
  name: #{name}
66
+ namespace: local
58
67
  description: Describe what this skill does
59
- model:
60
- tools: []
68
+ trigger: on_demand
61
69
  ---
62
70
 
63
71
  You are a helpful assistant. Describe the skill's behavior here.
64
72
  SKILL
65
73
 
66
- File.write(path, content)
74
+ ::File.write(path, content)
67
75
  say "Created: #{path}", :green
68
76
  end
69
77
 
70
- desc 'execute NAME [INPUT]', 'Run a skill outside of chat'
71
- map 'run' => :execute
72
- def execute(name, *input)
73
- require 'legion/chat/skills'
74
- skill = Legion::Chat::Skills.find(name)
75
- unless skill
76
- say "Skill '#{name}' not found", :red
77
- return
78
- end
78
+ desc 'run NAME', 'Run a skill via the daemon'
79
+ map 'run' => :run_skill
80
+ def run_skill(name)
81
+ url = "#{daemon_base_url}/api/skills/invoke"
82
+ payload = { skill_name: name }.to_json
79
83
 
80
- user_input = input.empty? ? nil : input.join(' ')
81
- result = Legion::Chat::Skills.execute(skill, input: user_input)
84
+ response = ::Net::HTTP.post(
85
+ ::URI.parse(url),
86
+ payload,
87
+ 'Content-Type' => 'application/json'
88
+ )
82
89
 
83
- if result[:success]
84
- say result[:output].to_s
90
+ if response.is_a?(::Net::HTTPSuccess)
91
+ result = ::JSON.parse(response.body, symbolize_names: true)
92
+ say result.dig(:data, :content).to_s
85
93
  else
86
- say "Skill failed: #{result[:error]}", :red
94
+ say "Error: #{response.code} #{response.body}", :red
95
+ exit 1
96
+ end
97
+ end
98
+
99
+ no_commands do
100
+ def daemon_base_url
101
+ host = Legion::Settings.dig(:api, :host) || 'localhost'
102
+ port = Legion::Settings.dig(:api, :port) || 4567
103
+ "http://#{host}:#{port}"
104
+ end
105
+
106
+ def daemon_get(path)
107
+ uri = ::URI.parse("#{daemon_base_url}#{path}")
108
+ ::Net::HTTP.get_response(uri)
87
109
  end
88
110
  end
89
111
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Builder
8
+ module Skills
9
+ include Legion::Extensions::Builder::Base
10
+
11
+ attr_reader :skills
12
+
13
+ def build_skills
14
+ return unless Object.const_defined?('Legion::LLM::Skills', false)
15
+ return unless Object.const_defined?('Legion::LLM', false) &&
16
+ Legion::LLM.respond_to?(:started?) && Legion::LLM.started?
17
+ return if Legion::LLM.settings.dig(:skills, :enabled) == false
18
+
19
+ @skills = {}
20
+ lex_mod = lex_class.is_a?(::Module) ? lex_class : ::Kernel.const_get(lex_class.to_s)
21
+ lex_mod.const_set(:Skills, ::Module.new) unless lex_mod.const_defined?(:Skills, false)
22
+ require_files(skill_files)
23
+ build_skill_list
24
+ end
25
+
26
+ def build_skill_list
27
+ skill_files.each do |file|
28
+ skill_name = file.split('/').last.sub('.rb', '')
29
+ skill_class_name = "#{lex_class}::Skills::#{skill_name.split('_').collect(&:capitalize).join}"
30
+ loaded_skill = Kernel.const_get(skill_class_name)
31
+ Legion::LLM::Skills::Registry.register(loaded_skill)
32
+ @skills[skill_name.to_sym] = {
33
+ skill_class: skill_class_name,
34
+ skill_module: loaded_skill
35
+ }
36
+ Legion::Logging.debug "[Skills] registered: #{skill_class_name}" if defined?(Legion::Logging)
37
+ end
38
+ end
39
+
40
+ def skill_files
41
+ @skill_files ||= find_files('skills')
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -7,6 +7,7 @@ require_relative 'builders/helpers'
7
7
  require_relative 'builders/hooks'
8
8
  require_relative 'builders/routes'
9
9
  require_relative 'builders/runners'
10
+ require_relative 'builders/skills'
10
11
 
11
12
  require_relative 'helpers/segments'
12
13
  require_relative 'helpers/core'
@@ -57,6 +58,7 @@ module Legion
57
58
  include Legion::Extensions::Builder::Actors
58
59
  include Legion::Extensions::Builder::Hooks
59
60
  include Legion::Extensions::Builder::Routes
61
+ include Legion::Extensions::Builder::Skills
60
62
 
61
63
  def autobuild
62
64
  Legion::Logging.debug "[Core] autobuild start: #{name}" if defined?(Legion::Logging)
@@ -81,6 +83,7 @@ module Legion
81
83
  build_actors
82
84
  build_hooks
83
85
  build_routes
86
+ build_skills if skills_required?
84
87
  Legion::Logging.debug "[Core] autobuild complete: #{name}" if defined?(Legion::Logging)
85
88
  end
86
89
 
@@ -108,6 +111,10 @@ module Legion
108
111
  false
109
112
  end
110
113
 
114
+ def skills_required?
115
+ false
116
+ end
117
+
111
118
  def remote_invocable?
112
119
  true
113
120
  end
@@ -266,6 +266,12 @@ module Legion
266
266
  return false
267
267
  end
268
268
 
269
+ if extension.respond_to?(:skills_required?) && extension.skills_required? &&
270
+ !Object.const_defined?('Legion::LLM::Skills', false)
271
+ Legion::Logging.warn "#{ext_name} requires Legion::LLM::Skills but isn't loaded, skipping"
272
+ return false
273
+ end
274
+
269
275
  has_logger = extension.respond_to?(:log)
270
276
  extension.autobuild
271
277
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.7.37'
4
+ VERSION = '1.8.0'
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.37
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -516,6 +516,7 @@ files:
516
516
  - lib/legion/api/router.rb
517
517
  - lib/legion/api/schedules.rb
518
518
  - lib/legion/api/settings.rb
519
+ - lib/legion/api/skills.rb
519
520
  - lib/legion/api/stats.rb
520
521
  - lib/legion/api/sync_dispatch.rb
521
522
  - lib/legion/api/tasks.rb
@@ -817,6 +818,7 @@ files:
817
818
  - lib/legion/extensions/builders/hooks.rb
818
819
  - lib/legion/extensions/builders/routes.rb
819
820
  - lib/legion/extensions/builders/runners.rb
821
+ - lib/legion/extensions/builders/skills.rb
820
822
  - lib/legion/extensions/capability.rb
821
823
  - lib/legion/extensions/catalog.rb
822
824
  - lib/legion/extensions/catalog/available.rb