legionio 1.7.6 → 1.7.12
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/.rubocop.yml +3 -1
- data/CHANGELOG.md +39 -0
- data/legionio.gemspec +8 -8
- data/lib/legion/api/audit.rb +1 -1
- data/lib/legion/api/default_settings.rb +48 -0
- data/lib/legion/api/events.rb +38 -15
- data/lib/legion/api/helpers.rb +29 -7
- data/lib/legion/api/library_routes.rb +3 -0
- data/lib/legion/api/llm.rb +5 -2
- data/lib/legion/api/tenants.rb +5 -6
- data/lib/legion/api/workers.rb +1 -14
- data/lib/legion/api.rb +22 -2
- data/lib/legion/cli/chat/chat_logger.rb +19 -12
- data/lib/legion/cli/check_command.rb +4 -2
- data/lib/legion/cli/config_command.rb +1 -1
- data/lib/legion/cli/error_handler.rb +8 -2
- data/lib/legion/cli/start.rb +3 -2
- data/lib/legion/cli.rb +13 -2
- data/lib/legion/context.rb +18 -0
- data/lib/legion/extensions/actors/base.rb +6 -3
- data/lib/legion/extensions/actors/every.rb +4 -3
- data/lib/legion/extensions/actors/loop.rb +1 -1
- data/lib/legion/extensions/actors/poll.rb +4 -4
- data/lib/legion/extensions/actors/subscription.rb +12 -9
- data/lib/legion/extensions/catalog.rb +77 -11
- data/lib/legion/extensions/core.rb +24 -7
- data/lib/legion/extensions/helpers/logger.rb +3 -62
- data/lib/legion/extensions/helpers/secret.rb +2 -0
- data/lib/legion/extensions/helpers/task.rb +4 -2
- data/lib/legion/extensions/transport.rb +18 -4
- data/lib/legion/ingress.rb +12 -8
- data/lib/legion/region.rb +36 -1
- data/lib/legion/runner.rb +34 -19
- data/lib/legion/service.rb +194 -135
- data/lib/legion/task_outcome_observer.rb +32 -8
- data/lib/legion/telemetry.rb +19 -11
- data/lib/legion/version.rb +1 -1
- data/lib/legion/webhooks.rb +169 -40
- metadata +18 -17
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 60c67de284b066264f07c3ad829b8d193b66d85ae900af468dd7a13165695da8
|
|
4
|
+
data.tar.gz: 8fd7ab97436343722619c6c77e0aaff9b6f664609db1b81a0daff97c6692f419
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2fde83c468f409a35fc9e02d1dc39d969a0229d32c8b592162789683f21afa31df90763792a1c4c74cb74f13ef21f1288e125d78b2b68ac2f9d724a08eee1e77
|
|
7
|
+
data.tar.gz: d10be584f4e30e1f164545c5c6e72f2e3cfa627e19675021edaf18ee6224d3f66d92ce50fcaf44b40caf0e47515e61d137e921f3903e5cab34254245fcd84bfa
|
data/.rubocop.yml
CHANGED
|
@@ -5,6 +5,8 @@ AllCops:
|
|
|
5
5
|
|
|
6
6
|
Layout/LineLength:
|
|
7
7
|
Max: 160
|
|
8
|
+
Exclude:
|
|
9
|
+
- 'Gemfile'
|
|
8
10
|
|
|
9
11
|
Layout/SpaceAroundEqualsInParameterDefault:
|
|
10
12
|
EnforcedStyle: space
|
|
@@ -65,7 +67,7 @@ Metrics/BlockLength:
|
|
|
65
67
|
- 'lib/legion/cli/mode_command.rb'
|
|
66
68
|
|
|
67
69
|
Metrics/AbcSize:
|
|
68
|
-
Max:
|
|
70
|
+
Max: 62
|
|
69
71
|
Exclude:
|
|
70
72
|
- 'lib/legion/cli/chat_command.rb'
|
|
71
73
|
- 'lib/legion/api/llm.rb'
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.7.12] - 2026-04-03
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Fixes #110: normal daemon boot now prefers library-owned LLM and Apollo API routes, `/api/tenants` uses canonical JSON parsing with correct status codes, SSE listeners drain worker threads on disconnect, paginated collections avoid unconditional `COUNT(*)` unless explicitly requested, and service startup skips duplicate settings loads once configuration is already bootstrapped
|
|
9
|
+
|
|
10
|
+
## [1.7.11] - 2026-04-02
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Fixes #113: webhook deliveries now retry non-2xx responses and transport exceptions up to `max_retries`, record per-attempt delivery rows, dead-letter terminal failures, and cache active webhook pattern matching to reduce per-event dispatch overhead
|
|
14
|
+
|
|
15
|
+
## [1.7.10] - 2026-04-02
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Bumped minimum dependency floors for Legion core gems, including `legion-logging >= 1.5.0`, `legion-settings >= 1.3.25`, and updated transport, data, cache, crypt, Apollo, and MCP minimums
|
|
19
|
+
- Stabilized the `LegionIO` spec suite by fixing the OAuth callback, catalog, and service shutdown regression specs
|
|
20
|
+
- CLI startup now honors settings-driven log levels, normalizes `start --help` into the standard Thor help flow, and routes chat/error logging through the newer helper-backed logger path
|
|
21
|
+
- `Legion::Service`, telemetry, and webhook runtime paths now use structured helper logging more consistently, respect configured logging when no CLI override is passed, and avoid brittle settings reads during boot
|
|
22
|
+
- Extension runtime wiring now deep-dups merged settings, lazily registers the local `extension_catalog` migration, publishes catalog transitions directly to transport, and surfaces auto-binding failures more clearly
|
|
23
|
+
- Secret, region, and task-outcome helpers now use canonical Vault connectivity checks, cache metadata misses more safely, and create meta-learning domains on demand before recording learning episodes
|
|
24
|
+
|
|
25
|
+
## [1.7.8] - 2026-04-01
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- `Legion::API::Settings` module with registered defaults via `merge_settings('api', ...)`, matching the pattern used by all other LegionIO gems
|
|
29
|
+
- Puma `persistent_timeout` (20s) and `first_data_timeout` (30s) now configurable via `Settings[:api][:puma]`
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- Removed all inline `||` and `.fetch(..., default)` fallbacks for API settings in `service.rb` and `check_command.rb` — defaults now guaranteed by `merge_settings`
|
|
33
|
+
|
|
34
|
+
## [1.7.7] - 2026-04-01
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
- Integrated legion-logging 1.4.3 Helper refactor: all log output now uses structured segment tagging, colored exception output, and thread-local task context
|
|
38
|
+
- Slimmed `Extensions::Helpers::Logger` to thin override; `derive_component_type`, `lex_gem_name`, `gem_spec_for_lex`, `log_lex_name` now live in legion-logging gem
|
|
39
|
+
- Added `handle_runner_exception` for runner-specific exception handling (TaskLog publish + HandledTask raise)
|
|
40
|
+
- Added `Legion::Context.with_task_context` and `.current_task_context` for thread-local task propagation
|
|
41
|
+
- Wrapped all 5 dispatch paths (Runner.run, Subscription#dispatch_runner, Base#runner, Ingress local/remote) with context propagation
|
|
42
|
+
- Migrated 13 `log.log_exception` call sites to `handle_exception` across actors, core, transport, and task helpers
|
|
43
|
+
|
|
5
44
|
## [1.7.6] - 2026-04-01
|
|
6
45
|
|
|
7
46
|
### Changed
|
data/legionio.gemspec
CHANGED
|
@@ -34,7 +34,7 @@ Gem::Specification.new do |spec|
|
|
|
34
34
|
spec.bindir = 'exe'
|
|
35
35
|
spec.executables = %w[legion legionio]
|
|
36
36
|
|
|
37
|
-
spec.add_dependency 'legion-mcp', '>= 0.
|
|
37
|
+
spec.add_dependency 'legion-mcp', '>= 0.7.1'
|
|
38
38
|
|
|
39
39
|
spec.add_dependency 'kramdown', '>= 2.0'
|
|
40
40
|
|
|
@@ -52,15 +52,15 @@ Gem::Specification.new do |spec|
|
|
|
52
52
|
spec.add_dependency 'thor', '>= 1.3'
|
|
53
53
|
spec.add_dependency 'tty-spinner', '~> 0.9'
|
|
54
54
|
|
|
55
|
-
spec.add_dependency 'legion-cache', '>= 1.3.
|
|
56
|
-
spec.add_dependency 'legion-crypt', '>= 1.
|
|
57
|
-
spec.add_dependency 'legion-data', '>= 1.6.
|
|
55
|
+
spec.add_dependency 'legion-cache', '>= 1.3.21'
|
|
56
|
+
spec.add_dependency 'legion-crypt', '>= 1.5.0'
|
|
57
|
+
spec.add_dependency 'legion-data', '>= 1.6.19'
|
|
58
58
|
spec.add_dependency 'legion-json', '>= 1.2.1'
|
|
59
|
-
spec.add_dependency 'legion-logging', '>= 1.
|
|
60
|
-
spec.add_dependency 'legion-settings', '>= 1.3.
|
|
61
|
-
spec.add_dependency 'legion-transport', '>= 1.4.
|
|
59
|
+
spec.add_dependency 'legion-logging', '>= 1.5.0'
|
|
60
|
+
spec.add_dependency 'legion-settings', '>= 1.3.25'
|
|
61
|
+
spec.add_dependency 'legion-transport', '>= 1.4.13'
|
|
62
62
|
|
|
63
|
-
spec.add_dependency 'legion-apollo', '>= 0.
|
|
63
|
+
spec.add_dependency 'legion-apollo', '>= 0.4.0'
|
|
64
64
|
spec.add_dependency 'legion-gaia', '>= 0.9.26'
|
|
65
65
|
spec.add_dependency 'legion-llm', '>= 0.5.8'
|
|
66
66
|
spec.add_dependency 'legion-tty', '>= 0.4.35'
|
data/lib/legion/api/audit.rb
CHANGED
|
@@ -4,7 +4,7 @@ module Legion
|
|
|
4
4
|
class API < Sinatra::Base
|
|
5
5
|
module Routes
|
|
6
6
|
module Audit
|
|
7
|
-
def self.registered(app)
|
|
7
|
+
def self.registered(app)
|
|
8
8
|
app.get '/api/audit' do
|
|
9
9
|
require_data!
|
|
10
10
|
dataset = Legion::Data::Model::AuditLog.order(Sequel.desc(:id))
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'sinatra/base'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
class API < Sinatra::Base
|
|
7
|
+
module Settings
|
|
8
|
+
def self.default
|
|
9
|
+
{
|
|
10
|
+
enabled: true,
|
|
11
|
+
port: 4567,
|
|
12
|
+
bind: '0.0.0.0',
|
|
13
|
+
puma: puma_defaults,
|
|
14
|
+
bind_retries: 3,
|
|
15
|
+
bind_retry_wait: 2,
|
|
16
|
+
tls: tls_defaults
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.puma_defaults
|
|
21
|
+
{
|
|
22
|
+
min_threads: 10,
|
|
23
|
+
max_threads: 16,
|
|
24
|
+
persistent_timeout: 20,
|
|
25
|
+
first_data_timeout: 30
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.tls_defaults
|
|
30
|
+
{
|
|
31
|
+
enabled: false
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
begin
|
|
39
|
+
Legion::Settings.merge_settings('api', Legion::API::Settings.default) if Legion.const_defined?('Settings', false)
|
|
40
|
+
rescue StandardError => e
|
|
41
|
+
if Legion.const_defined?('Logging', false) && Legion::Logging.respond_to?(:fatal)
|
|
42
|
+
Legion::Logging.fatal(e.message)
|
|
43
|
+
Legion::Logging.fatal(e.backtrace)
|
|
44
|
+
else
|
|
45
|
+
puts e.message
|
|
46
|
+
puts e.backtrace
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/legion/api/events.rb
CHANGED
|
@@ -5,6 +5,7 @@ module Legion
|
|
|
5
5
|
module Routes
|
|
6
6
|
module Events
|
|
7
7
|
BUFFER_SIZE = 100
|
|
8
|
+
SSE_STOP = Object.new.freeze
|
|
8
9
|
|
|
9
10
|
class << self
|
|
10
11
|
def event_buffer
|
|
@@ -38,6 +39,42 @@ module Legion
|
|
|
38
39
|
@listener_installed = true
|
|
39
40
|
end
|
|
40
41
|
|
|
42
|
+
def write_sse_event(out, event)
|
|
43
|
+
payload = event.transform_keys(&:to_s)
|
|
44
|
+
out << "event: #{payload['event']}\ndata: #{Legion::JSON.dump(payload)}\n\n"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def stop_queue_stream(queue:, worker:, listener:)
|
|
48
|
+
Legion::Events.off('*', listener) if defined?(Legion::Events)
|
|
49
|
+
return unless worker&.alive?
|
|
50
|
+
|
|
51
|
+
queue.push(SSE_STOP)
|
|
52
|
+
worker.join(0.1)
|
|
53
|
+
rescue ThreadError, IOError, Errno::EPIPE => e
|
|
54
|
+
Legion::Logging.debug("Events SSE cleanup failed: #{e.message}") if defined?(Legion::Logging)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def stream_queue(out:, queue:, listener:)
|
|
58
|
+
worker = Thread.new do
|
|
59
|
+
loop do
|
|
60
|
+
event = queue.pop
|
|
61
|
+
break if event.equal?(SSE_STOP)
|
|
62
|
+
|
|
63
|
+
write_sse_event(out, event)
|
|
64
|
+
rescue IOError, Errno::EPIPE => e
|
|
65
|
+
Legion::Logging.debug("Events SSE stream broken for #{event[:event]}: #{e.message}") if defined?(Legion::Logging)
|
|
66
|
+
break
|
|
67
|
+
end
|
|
68
|
+
ensure
|
|
69
|
+
Legion::Events.off('*', listener) if defined?(Legion::Events)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
cleanup = proc { stop_queue_stream(queue: queue, worker: worker, listener: listener) }
|
|
73
|
+
out.callback(&cleanup)
|
|
74
|
+
out.errback(&cleanup)
|
|
75
|
+
worker
|
|
76
|
+
end
|
|
77
|
+
|
|
41
78
|
def registered(app)
|
|
42
79
|
install_listener if defined?(Legion::Events)
|
|
43
80
|
|
|
@@ -53,21 +90,7 @@ module Legion
|
|
|
53
90
|
end
|
|
54
91
|
|
|
55
92
|
stream do |out|
|
|
56
|
-
|
|
57
|
-
loop do
|
|
58
|
-
event = queue.pop
|
|
59
|
-
data = Legion::JSON.dump(event.transform_keys(&:to_s))
|
|
60
|
-
out << "event: #{event[:event]}\ndata: #{data}\n\n"
|
|
61
|
-
rescue IOError, Errno::EPIPE => e
|
|
62
|
-
Legion::Logging.debug "Events SSE stream broken for #{event[:event]}: #{e.message}" if defined?(Legion::Logging)
|
|
63
|
-
break
|
|
64
|
-
end
|
|
65
|
-
ensure
|
|
66
|
-
Legion::Events.off('*', listener)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
out.callback { Legion::Events.off('*', listener) }
|
|
70
|
-
out.errback { Legion::Events.off('*', listener) }
|
|
93
|
+
stream_queue(out: out, queue: queue, listener: listener)
|
|
71
94
|
end
|
|
72
95
|
end
|
|
73
96
|
|
data/lib/legion/api/helpers.rb
CHANGED
|
@@ -16,17 +16,20 @@ module Legion
|
|
|
16
16
|
content_type :json
|
|
17
17
|
status status_code
|
|
18
18
|
|
|
19
|
-
total = dataset.respond_to?(:count) ? dataset.count : dataset.length
|
|
20
19
|
paginated = paginate(dataset)
|
|
21
|
-
items = paginated.respond_to?(:all) ? paginated.all : paginated
|
|
20
|
+
items = paginated.respond_to?(:all) ? paginated.all : Array(paginated)
|
|
21
|
+
total = collection_total(dataset, items)
|
|
22
|
+
meta = response_meta.merge(
|
|
23
|
+
count: items.length,
|
|
24
|
+
limit: page_limit,
|
|
25
|
+
offset: page_offset
|
|
26
|
+
)
|
|
27
|
+
meta[:total] = total unless total.nil?
|
|
28
|
+
meta[:has_more] = collection_has_more?(items, total)
|
|
22
29
|
|
|
23
30
|
Legion::JSON.dump({
|
|
24
31
|
data: items.map { |r| r.respond_to?(:values) ? r.values : r },
|
|
25
|
-
meta:
|
|
26
|
-
total: total,
|
|
27
|
-
limit: page_limit,
|
|
28
|
-
offset: page_offset
|
|
29
|
-
)
|
|
32
|
+
meta: meta
|
|
30
33
|
})
|
|
31
34
|
end
|
|
32
35
|
|
|
@@ -198,6 +201,25 @@ module Legion
|
|
|
198
201
|
dataset
|
|
199
202
|
end
|
|
200
203
|
end
|
|
204
|
+
|
|
205
|
+
def include_total_count?
|
|
206
|
+
params[:include_total].to_s == 'true'
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def collection_total(dataset, items)
|
|
210
|
+
return dataset.count if include_total_count? && dataset.respond_to?(:count)
|
|
211
|
+
return dataset.length if dataset.respond_to?(:length) && !dataset.respond_to?(:limit)
|
|
212
|
+
|
|
213
|
+
return page_offset + items.length if items.length < page_limit
|
|
214
|
+
|
|
215
|
+
nil
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def collection_has_more?(items, total)
|
|
219
|
+
return (page_offset + items.length) < total if total
|
|
220
|
+
|
|
221
|
+
items.length == page_limit
|
|
222
|
+
end
|
|
201
223
|
end
|
|
202
224
|
end
|
|
203
225
|
end
|
|
@@ -11,6 +11,9 @@ module Legion
|
|
|
11
11
|
# @param gem_name [String] short name for the library (e.g. 'llm', 'apollo')
|
|
12
12
|
# @param routes_module [Module] a Sinatra::Extension module to register
|
|
13
13
|
def self.register_library_routes(gem_name, routes_module)
|
|
14
|
+
existing = router.library_routes[gem_name.to_s]
|
|
15
|
+
return routes_module if existing == routes_module
|
|
16
|
+
|
|
14
17
|
router.register_library(gem_name, routes_module)
|
|
15
18
|
register routes_module
|
|
16
19
|
end
|
data/lib/legion/api/llm.rb
CHANGED
|
@@ -366,8 +366,11 @@ module Legion
|
|
|
366
366
|
stream do |out|
|
|
367
367
|
full_text = +''
|
|
368
368
|
pipeline_response = executor.call_stream do |chunk|
|
|
369
|
-
|
|
370
|
-
|
|
369
|
+
text = chunk.respond_to?(:content) ? chunk.content.to_s : chunk.to_s
|
|
370
|
+
next if text.empty?
|
|
371
|
+
|
|
372
|
+
full_text << text
|
|
373
|
+
out << "event: text-delta\ndata: #{Legion::JSON.dump({ delta: text })}\n\n"
|
|
371
374
|
end
|
|
372
375
|
|
|
373
376
|
if pipeline_response.tools.is_a?(Array) && !pipeline_response.tools.empty?
|
data/lib/legion/api/tenants.rb
CHANGED
|
@@ -13,14 +13,13 @@ module Legion
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
app.post '/api/tenants' do
|
|
16
|
-
|
|
16
|
+
body = parse_request_body
|
|
17
17
|
result = Legion::Tenants.create(
|
|
18
|
-
tenant_id:
|
|
19
|
-
name:
|
|
20
|
-
max_workers:
|
|
18
|
+
tenant_id: body[:tenant_id],
|
|
19
|
+
name: body[:name],
|
|
20
|
+
max_workers: body[:max_workers] || 10
|
|
21
21
|
)
|
|
22
|
-
|
|
23
|
-
json_response(data: result)
|
|
22
|
+
json_response(result, status_code: result[:error] ? 409 : 201)
|
|
24
23
|
end
|
|
25
24
|
|
|
26
25
|
app.get '/api/tenants/:tenant_id' do
|
data/lib/legion/api/workers.rb
CHANGED
|
@@ -167,20 +167,7 @@ module Legion
|
|
|
167
167
|
end
|
|
168
168
|
|
|
169
169
|
stream do |out|
|
|
170
|
-
|
|
171
|
-
loop do
|
|
172
|
-
event = queue.pop
|
|
173
|
-
data = Legion::JSON.dump({ **event.transform_keys(&:to_s) })
|
|
174
|
-
out << "event: #{event[:event]}\ndata: #{data}\n\n"
|
|
175
|
-
rescue IOError, Errno::EPIPE
|
|
176
|
-
break
|
|
177
|
-
end
|
|
178
|
-
ensure
|
|
179
|
-
Legion::Events.off('*', listener)
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
out.callback { Legion::Events.off('*', listener) }
|
|
183
|
-
out.errback { Legion::Events.off('*', listener) }
|
|
170
|
+
Routes::Events.stream_queue(out: out, queue: queue, listener: listener)
|
|
184
171
|
end
|
|
185
172
|
else
|
|
186
173
|
count = (params[:count] || 25).to_i
|
data/lib/legion/api.rb
CHANGED
|
@@ -4,6 +4,7 @@ require 'sinatra/base'
|
|
|
4
4
|
require 'legion/json'
|
|
5
5
|
require_relative 'events'
|
|
6
6
|
require_relative 'readiness'
|
|
7
|
+
require_relative 'api/default_settings'
|
|
7
8
|
|
|
8
9
|
require_relative 'api/middleware/auth'
|
|
9
10
|
require_relative 'api/middleware/body_limit'
|
|
@@ -148,6 +149,25 @@ module Legion
|
|
|
148
149
|
def router
|
|
149
150
|
@router ||= Legion::API::Router.new
|
|
150
151
|
end
|
|
152
|
+
|
|
153
|
+
def mount_library_routes(gem_name, fallback_module, preferred_constant_path)
|
|
154
|
+
preferred = constant_from_path(preferred_constant_path)
|
|
155
|
+
if preferred.is_a?(Module)
|
|
156
|
+
register_library_routes(gem_name, preferred)
|
|
157
|
+
elsif router.library_names.include?(gem_name)
|
|
158
|
+
register_library_routes(gem_name, router.library_routes.fetch(gem_name))
|
|
159
|
+
else
|
|
160
|
+
register fallback_module
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
private
|
|
165
|
+
|
|
166
|
+
def constant_from_path(path)
|
|
167
|
+
path.to_s.split('::').reject(&:empty?).reduce(Object) { |scope, name| scope.const_get(name) }
|
|
168
|
+
rescue NameError
|
|
169
|
+
nil
|
|
170
|
+
end
|
|
151
171
|
end
|
|
152
172
|
|
|
153
173
|
# Mount route modules
|
|
@@ -174,14 +194,14 @@ module Legion
|
|
|
174
194
|
register Routes::Capacity
|
|
175
195
|
register Routes::Audit
|
|
176
196
|
register Routes::Metrics
|
|
177
|
-
|
|
197
|
+
mount_library_routes('llm', Routes::Llm, 'Legion::LLM::Routes')
|
|
178
198
|
register Routes::ExtensionCatalog
|
|
179
199
|
register Routes::OrgChart
|
|
180
200
|
register Routes::Governance
|
|
181
201
|
register Routes::Acp
|
|
182
202
|
register Routes::Prompts
|
|
183
203
|
register Routes::Marketplace
|
|
184
|
-
|
|
204
|
+
mount_library_routes('apollo', Routes::Apollo, 'Legion::Apollo::Routes')
|
|
185
205
|
register Routes::Costs
|
|
186
206
|
register Routes::Traces
|
|
187
207
|
register Routes::Stats
|
|
@@ -8,8 +8,14 @@ module Legion
|
|
|
8
8
|
module CLI
|
|
9
9
|
class Chat
|
|
10
10
|
module ChatLogger
|
|
11
|
-
LOG_DIR
|
|
11
|
+
LOG_DIR = File.expand_path('~/.legion')
|
|
12
12
|
LOG_FILE = File.join(LOG_DIR, 'legion-chat.log')
|
|
13
|
+
LEVELS = {
|
|
14
|
+
'debug' => ::Logger::DEBUG,
|
|
15
|
+
'info' => ::Logger::INFO,
|
|
16
|
+
'warn' => ::Logger::WARN,
|
|
17
|
+
'error' => ::Logger::ERROR
|
|
18
|
+
}.freeze
|
|
13
19
|
|
|
14
20
|
class << self
|
|
15
21
|
attr_reader :logger
|
|
@@ -22,20 +28,21 @@ module Legion
|
|
|
22
28
|
@logger
|
|
23
29
|
end
|
|
24
30
|
|
|
25
|
-
def debug(msg)
|
|
26
|
-
|
|
27
|
-
def
|
|
28
|
-
|
|
31
|
+
def debug(msg) = logger&.debug(msg)
|
|
32
|
+
|
|
33
|
+
def info(msg) = logger&.info(msg)
|
|
34
|
+
|
|
35
|
+
def warn(msg) = logger&.warn(msg)
|
|
36
|
+
|
|
37
|
+
def error(msg) = logger&.error(msg)
|
|
29
38
|
|
|
30
39
|
private
|
|
31
40
|
|
|
32
|
-
def parse_level(level)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
else ::Logger::INFO
|
|
38
|
-
end
|
|
41
|
+
def parse_level(level = 'info')
|
|
42
|
+
normalized_level = level.to_s.strip.downcase
|
|
43
|
+
return ::Logger::INFO if normalized_level.empty?
|
|
44
|
+
|
|
45
|
+
LEVELS.fetch(normalized_level, ::Logger::INFO)
|
|
39
46
|
end
|
|
40
47
|
|
|
41
48
|
def format_entry(severity, datetime, _progname, msg)
|
|
@@ -260,8 +260,10 @@ module Legion
|
|
|
260
260
|
|
|
261
261
|
def check_api(_options)
|
|
262
262
|
require 'legion/api'
|
|
263
|
-
|
|
264
|
-
|
|
263
|
+
api_settings = Legion::Settings[:api]
|
|
264
|
+
port = api_settings[:port]
|
|
265
|
+
configured_bind = api_settings[:bind]
|
|
266
|
+
bind = %w[127.0.0.1 localhost ::1].include?(configured_bind) ? configured_bind : '127.0.0.1'
|
|
265
267
|
|
|
266
268
|
Legion::API.set :port, port
|
|
267
269
|
Legion::API.set :bind, bind
|
|
@@ -101,7 +101,7 @@ module Legion
|
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
desc 'validate', 'Validate current configuration'
|
|
104
|
-
def validate # rubocop:disable Metrics/
|
|
104
|
+
def validate # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
105
105
|
out = formatter
|
|
106
106
|
Connection.config_dir = options[:config_dir] if options[:config_dir]
|
|
107
107
|
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging'
|
|
4
|
+
|
|
3
5
|
module Legion
|
|
4
6
|
module CLI
|
|
5
7
|
module ErrorHandler
|
|
8
|
+
extend Legion::Logging::Helper
|
|
9
|
+
|
|
6
10
|
PATTERNS = [
|
|
7
11
|
{
|
|
8
12
|
match: /connection refused.*5672|ECONNREFUSED.*5672|bunny.*not connected/i,
|
|
@@ -71,11 +75,13 @@ module Legion
|
|
|
71
75
|
def wrap(error)
|
|
72
76
|
pattern = PATTERNS.find { |p| error.message.match?(p[:match]) }
|
|
73
77
|
unless pattern
|
|
74
|
-
|
|
78
|
+
handle_exception(error, level: :error, handled: true, operation: :wrap_cli_error, matched: false) if logging_available?
|
|
79
|
+
log.error("[CLI] unhandled error: #{error.class} - #{error.message}") if logging_available?
|
|
75
80
|
return error
|
|
76
81
|
end
|
|
77
82
|
|
|
78
|
-
|
|
83
|
+
handle_exception(error, level: :warn, handled: true, operation: :wrap_cli_error, code: pattern[:code]) if logging_available?
|
|
84
|
+
log.warn("[CLI] matched error pattern :#{pattern[:code]} - #{error.message}") if logging_available?
|
|
79
85
|
Error.actionable(
|
|
80
86
|
code: pattern[:code],
|
|
81
87
|
message: "#{pattern[:message]}: #{error.message}",
|
data/lib/legion/cli/start.rb
CHANGED
|
@@ -10,7 +10,7 @@ module Legion
|
|
|
10
10
|
ENV['LEGION_LOCAL'] = 'true'
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
log_level = options[:log_level]
|
|
13
|
+
log_level = options[:log_level]
|
|
14
14
|
|
|
15
15
|
# Load settings early, before any legion-* gem requires can trigger auto-load.
|
|
16
16
|
# This ensures DNS bootstrap and config file loading happen exactly once.
|
|
@@ -26,7 +26,8 @@ module Legion
|
|
|
26
26
|
clear_log_file unless options[:daemonize]
|
|
27
27
|
|
|
28
28
|
api = options.fetch(:api, true)
|
|
29
|
-
service_opts = {
|
|
29
|
+
service_opts = { api: api }
|
|
30
|
+
service_opts[:log_level] = log_level if log_level
|
|
30
31
|
service_opts[:http_port] = options[:http_port] if options[:http_port]
|
|
31
32
|
service_opts[:role] = :lite if options[:lite]
|
|
32
33
|
Legion.instance_variable_set(:@service, Legion::Service.new(**service_opts))
|
data/lib/legion/cli.rb
CHANGED
|
@@ -90,7 +90,7 @@ module Legion
|
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
def self.start(given_args = ARGV, config = {})
|
|
93
|
-
super
|
|
93
|
+
super(normalize_help_args(given_args), config)
|
|
94
94
|
rescue Legion::CLI::Error => e
|
|
95
95
|
Legion::Logging.error("CLI::Main.start CLI error: #{e.message}") if defined?(Legion::Logging)
|
|
96
96
|
formatter = Output::Formatter.new(json: given_args.include?('--json'), color: !given_args.include?('--no-color'))
|
|
@@ -106,6 +106,17 @@ module Legion
|
|
|
106
106
|
exit(1)
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
+
def self.normalize_help_args(given_args)
|
|
110
|
+
args = Array(given_args).dup
|
|
111
|
+
return args unless args.length == 2
|
|
112
|
+
return args unless %w[--help -h].include?(args.last)
|
|
113
|
+
|
|
114
|
+
command = args.first
|
|
115
|
+
return args if command.start_with?('-') || command == 'help'
|
|
116
|
+
|
|
117
|
+
['help', command]
|
|
118
|
+
end
|
|
119
|
+
|
|
109
120
|
LEGION_GEMS = %w[
|
|
110
121
|
legion-transport legion-cache legion-crypt legion-data
|
|
111
122
|
legion-json legion-logging legion-settings
|
|
@@ -163,7 +174,7 @@ module Legion
|
|
|
163
174
|
option :pidfile, type: :string, aliases: ['-p'], desc: 'PID file path'
|
|
164
175
|
option :logfile, type: :string, aliases: ['-l'], desc: 'Log file path'
|
|
165
176
|
option :time_limit, type: :numeric, aliases: ['-t'], desc: 'Run for N seconds then exit'
|
|
166
|
-
option :log_level, type: :string,
|
|
177
|
+
option :log_level, type: :string, desc: 'Log level (debug, info, warn, error)'
|
|
167
178
|
option :api, type: :boolean, default: true, desc: 'Start the HTTP API server'
|
|
168
179
|
option :http_port, type: :numeric, desc: 'HTTP API port (overrides settings)'
|
|
169
180
|
option :lite, type: :boolean, default: false, desc: 'Start in lite mode (no external services)'
|
data/lib/legion/context.rb
CHANGED
|
@@ -51,6 +51,24 @@ module Legion
|
|
|
51
51
|
Legion::Logging.debug "[Context] session cleared: #{ctx&.session_id}" if defined?(Legion::Logging)
|
|
52
52
|
Thread.current[:legion_session_context] = nil
|
|
53
53
|
end
|
|
54
|
+
|
|
55
|
+
def current_task_context
|
|
56
|
+
Thread.current[:legion_context]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def with_task_context(message)
|
|
60
|
+
previous = Thread.current[:legion_context]
|
|
61
|
+
Thread.current[:legion_context] = {
|
|
62
|
+
task_id: message[:task_id],
|
|
63
|
+
conversation_id: message[:conversation_id],
|
|
64
|
+
chain_id: message[:chain_id],
|
|
65
|
+
function: message[:function],
|
|
66
|
+
runner_class: message[:runner_class]
|
|
67
|
+
}.compact
|
|
68
|
+
yield
|
|
69
|
+
ensure
|
|
70
|
+
Thread.current[:legion_context] = previous
|
|
71
|
+
end
|
|
54
72
|
end
|
|
55
73
|
end
|
|
56
74
|
end
|
|
@@ -16,9 +16,12 @@ module Legion
|
|
|
16
16
|
define_dsl_accessor :remote_invocable, default: true
|
|
17
17
|
|
|
18
18
|
def runner
|
|
19
|
-
|
|
19
|
+
with_log_context(function) do
|
|
20
|
+
Legion::Runner.run(runner_class: runner_class, function: function,
|
|
21
|
+
check_subtask: check_subtask?, generate_task: generate_task?)
|
|
22
|
+
end
|
|
20
23
|
rescue StandardError => e
|
|
21
|
-
|
|
24
|
+
handle_exception(e)
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
def manual
|
|
@@ -31,7 +34,7 @@ module Legion
|
|
|
31
34
|
klass.send(func, **args)
|
|
32
35
|
end
|
|
33
36
|
rescue StandardError => e
|
|
34
|
-
|
|
37
|
+
handle_exception(e)
|
|
35
38
|
end
|
|
36
39
|
|
|
37
40
|
def function
|
|
@@ -24,7 +24,8 @@ module Legion
|
|
|
24
24
|
log.debug "[Every] tick: #{self.class}" if defined?(log)
|
|
25
25
|
skip_or_run { use_runner? ? runner : manual }
|
|
26
26
|
rescue StandardError => e
|
|
27
|
-
log.
|
|
27
|
+
log.error "[Every] tick failed for #{self.class}: #{e.class}: #{e.message}" if defined?(log)
|
|
28
|
+
handle_exception(e) if defined?(log)
|
|
28
29
|
ensure
|
|
29
30
|
@executing.make_false
|
|
30
31
|
end
|
|
@@ -35,7 +36,7 @@ module Legion
|
|
|
35
36
|
|
|
36
37
|
@timer.execute
|
|
37
38
|
rescue StandardError => e
|
|
38
|
-
|
|
39
|
+
handle_exception(e)
|
|
39
40
|
end
|
|
40
41
|
|
|
41
42
|
def run_now?
|
|
@@ -52,7 +53,7 @@ module Legion
|
|
|
52
53
|
|
|
53
54
|
@timer.shutdown
|
|
54
55
|
rescue StandardError => e
|
|
55
|
-
|
|
56
|
+
handle_exception(e)
|
|
56
57
|
end
|
|
57
58
|
end
|
|
58
59
|
end
|