legion-tty 0.4.38 → 0.4.40
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 +21 -0
- data/lib/legion/tty/app.rb +10 -9
- data/lib/legion/tty/background/llm_probe.rb +26 -2
- data/lib/legion/tty/components/wizard_prompt.rb +6 -2
- data/lib/legion/tty/daemon_client.rb +38 -0
- data/lib/legion/tty/screens/chat/model_commands.rb +7 -23
- data/lib/legion/tty/screens/chat.rb +76 -63
- data/lib/legion/tty/screens/onboarding.rb +83 -25
- data/lib/legion/tty/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: 60e92790f0f6281b48346af3972e2631f1952b3b68f3d51922aa52da8682cfc9
|
|
4
|
+
data.tar.gz: 85491c70182ced9142b483c4edd903f3e5948bac1544c01cba69a86f565d2483
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 757d74c3e7e0d7711cc9bb0cb410c145e7e8ca31d484d62ad53160df64c34205be19d6eb0fa91d7aede125af63ff1d005f596ea10c285fcbdc97adb5c74eee19
|
|
7
|
+
data.tar.gz: df6acc61f03cfc4631ccd28eaea40813314aadc8211497e857507ac7453ab700952bf6588a4d34016d3d5196651680583a293ff1bfba204101408b62a5c7fd4d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.40] - 2026-03-28
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- All LLM calls in the chat screen now route exclusively through the LegionIO daemon API (`POST /api/llm/inference`) via `Legion::TTY::DaemonClient.inference`
|
|
7
|
+
- Removed soft fallback to raw RubyLLM (`send_via_direct` / `@llm_chat.ask`) — if the daemon is not running, a clear error is displayed: "LegionIO daemon is not running. Start it with: legionio start"
|
|
8
|
+
- Fixed `send_via_daemon`: was calling `Legion::LLM.ask` (which never returns `{status: :done}`) and falling through to direct on every call; now calls `DaemonClient.inference` with the full conversation history
|
|
9
|
+
- Fixed `daemon_available?`: was checking `Legion::LLM::DaemonClient` (an unrelated module); now calls `Legion::TTY::DaemonClient.available?` directly
|
|
10
|
+
- `try_settings_llm` in `App` no longer creates a raw `Legion::LLM.chat` session; daemon availability is logged instead
|
|
11
|
+
- System prompt is now injected per-request as part of the messages array sent to `/api/llm/inference`, not pre-set on a session object
|
|
12
|
+
- Added `DaemonClient.inference` method: `POST /api/llm/inference` with messages array, tools, model, provider; returns `{status: :ok/:error/:unavailable, data:}`
|
|
13
|
+
- Added `build_inference_messages`: assembles system + conversation history + current message for the inference call
|
|
14
|
+
- Added `track_inference_tokens`: tracks input/output tokens from the `data` hash returned by `/api/llm/inference`
|
|
15
|
+
|
|
16
|
+
## [0.4.39] - 2026-03-28
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Onboarding wizard now installs GAIA cognitive extension gems when waking GAIA: after the daemon starts (or is already running), `offer_gaia_gems` checks for the 21 agentic/GAIA-tier gems via `Gem::Specification.find_by_name` and prompts the user to install any that are missing
|
|
20
|
+
- `LlmProbe` now returns `:configured` status (instead of `:error`) when a provider is enabled but the ping fails (e.g. Vault-injected providers unknown to `apply_provider_config`); these providers are shown with a key icon and "configured, not validated" label and are selectable as the default when no fully-validated `:ok` providers exist
|
|
21
|
+
- `LlmProbe` accepts a `wait_queue:` argument so it waits for the bootstrap config thread to complete before probing, ensuring Vault-sourced provider settings are written to `~/.legionio/settings/llm.json` first
|
|
22
|
+
- `select_provider_default` falls back to `:configured` providers when no `:ok` providers exist, so Vault-resolved providers are usable as the chat default
|
|
23
|
+
|
|
3
24
|
## [0.4.38] - 2026-03-26
|
|
4
25
|
|
|
5
26
|
### Added
|
data/lib/legion/tty/app.rb
CHANGED
|
@@ -468,15 +468,16 @@ module Legion
|
|
|
468
468
|
end
|
|
469
469
|
|
|
470
470
|
def try_settings_llm
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
471
|
+
# All LLM calls route through the LegionIO daemon API.
|
|
472
|
+
# No raw RubyLLM session is created here — nil signals "use daemon path".
|
|
473
|
+
if Legion::TTY::DaemonClient.available?
|
|
474
|
+
Legion::Logging.debug('TTY: daemon available, LLM routed through daemon') if defined?(Legion::Logging)
|
|
475
|
+
elsif defined?(Legion::Logging)
|
|
476
|
+
if defined?(Legion::Logging)
|
|
477
|
+
Legion::Logging.warn('TTY: daemon not running; LLM unavailable until daemon starts')
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
nil
|
|
480
481
|
rescue StandardError => e
|
|
481
482
|
Legion::Logging.warn("try_settings_llm failed: #{e.message}") if defined?(Legion::Logging)
|
|
482
483
|
nil
|
|
@@ -4,12 +4,14 @@ module Legion
|
|
|
4
4
|
module TTY
|
|
5
5
|
module Background
|
|
6
6
|
class LlmProbe
|
|
7
|
-
def initialize(logger: nil)
|
|
7
|
+
def initialize(logger: nil, wait_queue: nil)
|
|
8
8
|
@log = logger
|
|
9
|
+
@wait_queue = wait_queue
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
def run_async(queue)
|
|
12
13
|
Thread.new do
|
|
14
|
+
wait_for_bootstrap if @wait_queue
|
|
13
15
|
result = probe_providers
|
|
14
16
|
queue.push({ data: result })
|
|
15
17
|
rescue StandardError => e
|
|
@@ -20,6 +22,28 @@ module Legion
|
|
|
20
22
|
|
|
21
23
|
private
|
|
22
24
|
|
|
25
|
+
def wait_for_bootstrap
|
|
26
|
+
deadline = Time.now + 15
|
|
27
|
+
timed_out = false
|
|
28
|
+
loop do
|
|
29
|
+
break unless @wait_queue.empty?
|
|
30
|
+
|
|
31
|
+
if Time.now >= deadline
|
|
32
|
+
timed_out = true
|
|
33
|
+
break
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
sleep 0.2
|
|
37
|
+
end
|
|
38
|
+
if timed_out
|
|
39
|
+
@log&.log('llm_probe', 'bootstrap wait timed out')
|
|
40
|
+
else
|
|
41
|
+
@log&.log('llm_probe', 'bootstrap wait complete')
|
|
42
|
+
end
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
@log&.log('llm_probe', "bootstrap wait error: #{e.message}")
|
|
45
|
+
end
|
|
46
|
+
|
|
23
47
|
def probe_providers
|
|
24
48
|
require 'legion/llm'
|
|
25
49
|
require 'legion/settings'
|
|
@@ -54,7 +78,7 @@ module Legion
|
|
|
54
78
|
rescue StandardError => e
|
|
55
79
|
latency = ((Time.now - start_time) * 1000).round
|
|
56
80
|
Legion::Logging.debug("ping_provider #{name} failed: #{e.message}") if defined?(Legion::Logging)
|
|
57
|
-
{ name: name, model: model, status: :
|
|
81
|
+
{ name: name, model: model, status: :configured, latency_ms: latency, error: e.message }
|
|
58
82
|
end
|
|
59
83
|
end
|
|
60
84
|
end
|
|
@@ -56,10 +56,14 @@ module Legion
|
|
|
56
56
|
|
|
57
57
|
def display_provider_results(providers)
|
|
58
58
|
providers.each do |p|
|
|
59
|
-
icon = p[:status]
|
|
59
|
+
icon = case p[:status]
|
|
60
|
+
when :ok then "\u2705"
|
|
61
|
+
when :configured then "\U0001F511"
|
|
62
|
+
else "\u274C"
|
|
63
|
+
end
|
|
60
64
|
latency = "#{p[:latency_ms]}ms"
|
|
61
65
|
label = "#{icon} #{p[:name]} (#{p[:model]}) \u2014 #{latency}"
|
|
62
|
-
label += " [#{p[:error]}]" if p[:error]
|
|
66
|
+
label += p[:status] == :configured ? ' [configured, not validated]' : " [#{p[:error]}]" if p[:error]
|
|
63
67
|
@prompt.say(label)
|
|
64
68
|
end
|
|
65
69
|
end
|
|
@@ -10,6 +10,7 @@ module Legion
|
|
|
10
10
|
module DaemonClient
|
|
11
11
|
SUCCESS_CODES = [200, 201, 202].freeze
|
|
12
12
|
|
|
13
|
+
# rubocop:disable Metrics/ClassLength
|
|
13
14
|
class << self
|
|
14
15
|
def configure(daemon_url: 'http://127.0.0.1:4567', cache_file: nil, timeout: 5)
|
|
15
16
|
@daemon_url = daemon_url
|
|
@@ -89,6 +90,19 @@ module Legion
|
|
|
89
90
|
nil
|
|
90
91
|
end
|
|
91
92
|
|
|
93
|
+
def inference(messages:, tools: [], model: nil, provider: nil, timeout: 120)
|
|
94
|
+
response = post_inference(messages: messages, tools: tools, model: model,
|
|
95
|
+
provider: provider, timeout: timeout)
|
|
96
|
+
return inference_error_result(response) unless SUCCESS_CODES.include?(response.code.to_i)
|
|
97
|
+
|
|
98
|
+
body = Legion::JSON.load(response.body)
|
|
99
|
+
data = body[:data] || body
|
|
100
|
+
{ status: :ok, data: data }
|
|
101
|
+
rescue StandardError => e
|
|
102
|
+
Legion::Logging.warn("inference failed: #{e.message}") if defined?(Legion::Logging)
|
|
103
|
+
{ status: :unavailable, error: { message: e.message } }
|
|
104
|
+
end
|
|
105
|
+
|
|
92
106
|
def reset!
|
|
93
107
|
@daemon_url = nil
|
|
94
108
|
@cache_file = nil
|
|
@@ -98,6 +112,29 @@ module Legion
|
|
|
98
112
|
|
|
99
113
|
private
|
|
100
114
|
|
|
115
|
+
def post_inference(messages:, tools:, model:, provider:, timeout:)
|
|
116
|
+
uri = URI("#{daemon_url}/api/llm/inference")
|
|
117
|
+
payload = Legion::JSON.dump({ messages: messages, tools: tools,
|
|
118
|
+
model: model, provider: provider }.compact)
|
|
119
|
+
http_timeout = [timeout, @timeout || 5].max
|
|
120
|
+
req = Net::HTTP::Post.new(uri)
|
|
121
|
+
req['Content-Type'] = 'application/json'
|
|
122
|
+
req.body = payload
|
|
123
|
+
Net::HTTP.start(uri.hostname, uri.port,
|
|
124
|
+
open_timeout: @timeout || 5,
|
|
125
|
+
read_timeout: http_timeout) { |h| h.request(req) }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def inference_error_result(response)
|
|
129
|
+
body = begin
|
|
130
|
+
Legion::JSON.load(response.body)
|
|
131
|
+
rescue StandardError
|
|
132
|
+
{}
|
|
133
|
+
end
|
|
134
|
+
err = body.dig(:error, :message) || body.dig(:data, :error, :message) || "HTTP #{response.code}"
|
|
135
|
+
{ status: :error, error: { message: err } }
|
|
136
|
+
end
|
|
137
|
+
|
|
101
138
|
def daemon_url
|
|
102
139
|
@daemon_url || 'http://127.0.0.1:4567'
|
|
103
140
|
end
|
|
@@ -119,6 +156,7 @@ module Legion
|
|
|
119
156
|
nil
|
|
120
157
|
end
|
|
121
158
|
end
|
|
159
|
+
# rubocop:enable Metrics/ClassLength
|
|
122
160
|
end
|
|
123
161
|
end
|
|
124
162
|
end
|
|
@@ -18,33 +18,18 @@ module Legion
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def switch_model(name)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
apply_model_switch(name)
|
|
21
|
+
@preferred_model = name
|
|
22
|
+
@status_bar.update(model: name)
|
|
23
|
+
@token_tracker.update_model(name)
|
|
24
|
+
@message_stream.add_message(role: :system,
|
|
25
|
+
content: "Model preference set to: #{name} (applied on next daemon request)")
|
|
27
26
|
rescue StandardError => e
|
|
28
27
|
Legion::Logging.warn("switch_model failed: #{e.message}") if defined?(Legion::Logging)
|
|
29
28
|
@message_stream.add_message(role: :system, content: "Failed to switch model: #{e.message}")
|
|
30
29
|
end
|
|
31
30
|
|
|
32
31
|
def apply_model_switch(name)
|
|
33
|
-
|
|
34
|
-
if new_chat
|
|
35
|
-
@llm_chat = new_chat
|
|
36
|
-
@status_bar.update(model: name)
|
|
37
|
-
@token_tracker.update_model(name)
|
|
38
|
-
@message_stream.add_message(role: :system, content: "Switched to provider: #{name}")
|
|
39
|
-
elsif @llm_chat.respond_to?(:with_model)
|
|
40
|
-
@llm_chat.with_model(name)
|
|
41
|
-
@status_bar.update(model: name)
|
|
42
|
-
@token_tracker.update_model(name)
|
|
43
|
-
@message_stream.add_message(role: :system, content: "Model switched to: #{name}")
|
|
44
|
-
else
|
|
45
|
-
@status_bar.update(model: name)
|
|
46
|
-
@message_stream.add_message(role: :system, content: "Model set to: #{name}")
|
|
47
|
-
end
|
|
32
|
+
switch_model(name)
|
|
48
33
|
end
|
|
49
34
|
|
|
50
35
|
def try_provider_switch(name)
|
|
@@ -75,9 +60,8 @@ module Legion
|
|
|
75
60
|
end
|
|
76
61
|
|
|
77
62
|
def show_current_model
|
|
78
|
-
model = @llm_chat.respond_to?(:model) ? @llm_chat.model : nil
|
|
79
63
|
provider = safe_config[:provider] || 'unknown'
|
|
80
|
-
info =
|
|
64
|
+
info = @preferred_model ? "#{@preferred_model} (#{provider})" : provider
|
|
81
65
|
@message_stream.add_message(role: :system, content: "Current model: #{info}")
|
|
82
66
|
end
|
|
83
67
|
|
|
@@ -73,10 +73,10 @@ module Legion
|
|
|
73
73
|
@output = output
|
|
74
74
|
@message_stream = Components::MessageStream.new
|
|
75
75
|
@status_bar = Components::StatusBar.new
|
|
76
|
-
@llm_chat =
|
|
76
|
+
@llm_chat = nil
|
|
77
77
|
@token_tracker = Components::TokenTracker.new(
|
|
78
78
|
provider: detect_provider,
|
|
79
|
-
model:
|
|
79
|
+
model: nil
|
|
80
80
|
)
|
|
81
81
|
@session_store = SessionStore.new
|
|
82
82
|
@session_name = 'default'
|
|
@@ -174,16 +174,14 @@ module Legion
|
|
|
174
174
|
end
|
|
175
175
|
|
|
176
176
|
def send_to_llm(message)
|
|
177
|
-
unless
|
|
178
|
-
@message_stream.append_streaming(
|
|
177
|
+
unless daemon_available?
|
|
178
|
+
@message_stream.append_streaming(
|
|
179
|
+
'LegionIO daemon is not running. Start it with: legionio start'
|
|
180
|
+
)
|
|
179
181
|
return
|
|
180
182
|
end
|
|
181
183
|
|
|
182
|
-
|
|
183
|
-
send_via_daemon(message)
|
|
184
|
-
else
|
|
185
|
-
send_via_direct(message)
|
|
186
|
-
end
|
|
184
|
+
send_via_daemon(message)
|
|
187
185
|
rescue StandardError => e
|
|
188
186
|
Legion::Logging.error("send_to_llm failed: #{e.message}") if defined?(Legion::Logging)
|
|
189
187
|
@status_bar.update(thinking: false)
|
|
@@ -239,60 +237,47 @@ module Legion
|
|
|
239
237
|
end
|
|
240
238
|
|
|
241
239
|
def setup_system_prompt
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
prompt = build_system_prompt(cfg)
|
|
246
|
-
@llm_chat.with_instructions(prompt) if @llm_chat.respond_to?(:with_instructions)
|
|
240
|
+
# System prompt is injected per-request in build_inference_messages.
|
|
241
|
+
# Nothing to do at activation time.
|
|
247
242
|
end
|
|
248
243
|
|
|
244
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
249
245
|
def send_via_daemon(message)
|
|
250
|
-
|
|
246
|
+
@status_bar.update(thinking: true)
|
|
247
|
+
@streaming = true
|
|
248
|
+
@app.render_frame if @app.respond_to?(:render_frame)
|
|
249
|
+
|
|
250
|
+
start_time = Time.now
|
|
251
|
+
messages = build_inference_messages(message)
|
|
252
|
+
result = Legion::TTY::DaemonClient.inference(
|
|
253
|
+
messages: messages,
|
|
254
|
+
model: @preferred_model
|
|
255
|
+
)
|
|
251
256
|
|
|
252
|
-
case result
|
|
253
|
-
when :
|
|
257
|
+
case result[:status]
|
|
258
|
+
when :ok
|
|
259
|
+
data = result[:data] || {}
|
|
260
|
+
content = data[:content].to_s
|
|
254
261
|
parser = build_tool_call_parser
|
|
255
|
-
parser.feed(
|
|
262
|
+
parser.feed(content)
|
|
256
263
|
parser.flush
|
|
257
|
-
|
|
264
|
+
record_response_time(Time.now - start_time)
|
|
265
|
+
track_inference_tokens(data)
|
|
266
|
+
speak_response(content) if @speak_mode
|
|
258
267
|
when :error
|
|
259
268
|
err = result.dig(:error, :message) || 'Unknown error'
|
|
260
269
|
@message_stream.append_streaming("\n[Daemon error: #{err}]")
|
|
261
|
-
|
|
262
|
-
|
|
270
|
+
when :unavailable
|
|
271
|
+
err = result.dig(:error, :message) || 'Daemon unavailable'
|
|
272
|
+
@message_stream.append_streaming(
|
|
273
|
+
"\nLegionIO daemon is not running. Start it with: legionio start\n[#{err}]"
|
|
274
|
+
)
|
|
263
275
|
end
|
|
264
|
-
rescue StandardError => e
|
|
265
|
-
Legion::Logging.warn("send_via_daemon failed: #{e.message}") if defined?(Legion::Logging)
|
|
266
|
-
send_via_direct(message)
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
# rubocop:disable Metrics/AbcSize
|
|
270
|
-
def send_via_direct(message)
|
|
271
|
-
return unless @llm_chat
|
|
272
|
-
|
|
273
|
-
@status_bar.update(thinking: true)
|
|
274
|
-
@streaming = true
|
|
275
|
-
@app.render_frame if @app.respond_to?(:render_frame)
|
|
276
|
-
start_time = Time.now
|
|
277
|
-
response_text = +''
|
|
278
|
-
parser = build_tool_call_parser
|
|
279
|
-
response = @llm_chat.ask(message) do |chunk|
|
|
280
|
-
@status_bar.update(thinking: false)
|
|
281
|
-
if chunk.content
|
|
282
|
-
response_text << chunk.content
|
|
283
|
-
parser.feed(chunk.content)
|
|
284
|
-
end
|
|
285
|
-
@app.render_frame if @app.respond_to?(:render_frame)
|
|
286
|
-
end
|
|
287
|
-
parser.flush
|
|
288
|
-
record_response_time(Time.now - start_time)
|
|
289
|
-
@status_bar.update(thinking: false)
|
|
290
|
-
track_response_tokens(response)
|
|
291
|
-
speak_response(response_text) if @speak_mode
|
|
292
276
|
ensure
|
|
277
|
+
@status_bar.update(thinking: false)
|
|
293
278
|
@streaming = false
|
|
294
279
|
end
|
|
295
|
-
# rubocop:enable Metrics/AbcSize
|
|
280
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
296
281
|
|
|
297
282
|
def speak_response(text)
|
|
298
283
|
return unless RUBY_PLATFORM =~ /darwin/
|
|
@@ -310,7 +295,47 @@ module Legion
|
|
|
310
295
|
end
|
|
311
296
|
|
|
312
297
|
def daemon_available?
|
|
313
|
-
|
|
298
|
+
Legion::TTY::DaemonClient.available?
|
|
299
|
+
rescue StandardError => e
|
|
300
|
+
Legion::Logging.debug("daemon_available? check failed: #{e.message}") if defined?(Legion::Logging)
|
|
301
|
+
false
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def build_inference_messages(current_message)
|
|
305
|
+
msgs = []
|
|
306
|
+
inject_system_message(msgs)
|
|
307
|
+
inject_history_messages(msgs)
|
|
308
|
+
msgs.pop if msgs.last&.dig(:role) == 'user'
|
|
309
|
+
msgs << { role: 'user', content: current_message }
|
|
310
|
+
msgs
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def inject_system_message(msgs)
|
|
314
|
+
prompt = build_system_prompt(safe_config)
|
|
315
|
+
msgs << { role: 'system', content: prompt } if prompt && !prompt.strip.empty?
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def inject_history_messages(msgs)
|
|
319
|
+
@message_stream.messages.each do |m|
|
|
320
|
+
next if m[:tool_panel]
|
|
321
|
+
next unless %i[user assistant].include?(m[:role])
|
|
322
|
+
|
|
323
|
+
content = m[:content].to_s
|
|
324
|
+
next if content.strip.empty?
|
|
325
|
+
|
|
326
|
+
msgs << { role: m[:role].to_s, content: content }
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def track_inference_tokens(data)
|
|
331
|
+
return unless data.is_a?(Hash) && (data[:input_tokens] || data[:output_tokens])
|
|
332
|
+
|
|
333
|
+
@token_tracker.track(
|
|
334
|
+
input_tokens: data[:input_tokens].to_i,
|
|
335
|
+
output_tokens: data[:output_tokens].to_i,
|
|
336
|
+
model: data[:model]&.to_s
|
|
337
|
+
)
|
|
338
|
+
update_status_bar_tokens
|
|
314
339
|
end
|
|
315
340
|
|
|
316
341
|
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
@@ -638,18 +663,6 @@ module Legion
|
|
|
638
663
|
update_status_bar_tokens
|
|
639
664
|
end
|
|
640
665
|
|
|
641
|
-
def track_daemon_tokens(result)
|
|
642
|
-
meta = result[:meta]
|
|
643
|
-
return unless meta.is_a?(Hash) && (meta[:tokens_in] || meta[:tokens_out])
|
|
644
|
-
|
|
645
|
-
@token_tracker.track(
|
|
646
|
-
input_tokens: meta[:tokens_in].to_i,
|
|
647
|
-
output_tokens: meta[:tokens_out].to_i,
|
|
648
|
-
model: meta[:model]&.to_s
|
|
649
|
-
)
|
|
650
|
-
update_status_bar_tokens
|
|
651
|
-
end
|
|
652
|
-
|
|
653
666
|
def update_status_bar_tokens
|
|
654
667
|
@status_bar.update(
|
|
655
668
|
tokens: @token_tracker.total_input_tokens + @token_tracker.total_output_tokens,
|
|
@@ -17,6 +17,17 @@ module Legion
|
|
|
17
17
|
class Onboarding < Base
|
|
18
18
|
TYPED_DELAY = 0.05
|
|
19
19
|
|
|
20
|
+
GAIA_GEMS = %w[
|
|
21
|
+
lex-agentic-self lex-agentic-affect lex-agentic-attention
|
|
22
|
+
lex-agentic-defense lex-agentic-executive lex-agentic-homeostasis
|
|
23
|
+
lex-agentic-imagination lex-agentic-inference lex-agentic-integration
|
|
24
|
+
lex-agentic-language lex-agentic-learning lex-agentic-memory
|
|
25
|
+
lex-agentic-social
|
|
26
|
+
lex-tick lex-extinction lex-mind-growth lex-mesh
|
|
27
|
+
lex-synapse lex-react
|
|
28
|
+
legion-gaia legion-apollo
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
20
31
|
def initialize(app, wizard: nil, output: $stdout, skip_rain: false)
|
|
21
32
|
super(app)
|
|
22
33
|
@wizard = wizard || Components::WizardPrompt.new
|
|
@@ -119,6 +130,7 @@ module Legion
|
|
|
119
130
|
|
|
120
131
|
def select_provider_default(providers)
|
|
121
132
|
working = providers.select { |p| p[:status] == :ok }
|
|
133
|
+
working = providers.select { |p| p[:status] == :configured } if working.empty?
|
|
122
134
|
if working.any?
|
|
123
135
|
default = @wizard.select_default_provider(working)
|
|
124
136
|
sleep 0.5
|
|
@@ -139,7 +151,7 @@ module Legion
|
|
|
139
151
|
@kerberos_probe.run_async(@kerberos_queue)
|
|
140
152
|
@github_probe.run_quick_async(@github_quick_queue)
|
|
141
153
|
require_relative '../background/llm_probe'
|
|
142
|
-
@llm_probe = Background::LlmProbe.new(logger: @log)
|
|
154
|
+
@llm_probe = Background::LlmProbe.new(logger: @log, wait_queue: @bootstrap_queue)
|
|
143
155
|
@llm_probe.run_async(@llm_queue)
|
|
144
156
|
@bootstrap_probe = Background::BootstrapConfig.new(logger: @log)
|
|
145
157
|
@bootstrap_probe.run_async(@bootstrap_queue)
|
|
@@ -196,37 +208,42 @@ module Legion
|
|
|
196
208
|
end
|
|
197
209
|
end
|
|
198
210
|
|
|
199
|
-
# rubocop:disable Metrics/AbcSize
|
|
200
211
|
def run_gaia_awakening
|
|
201
212
|
typed_output('Scanning for active cognition threads...')
|
|
202
213
|
sleep 1.2
|
|
203
214
|
@output.puts
|
|
204
215
|
|
|
205
|
-
if legionio_running?
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if @wizard.confirm('Shall I wake her?')
|
|
216
|
-
started = start_legionio_daemon
|
|
217
|
-
if started
|
|
218
|
-
typed_output('... initializing cognitive substrate...')
|
|
219
|
-
sleep 1
|
|
220
|
-
typed_output('GAIA online. All systems nominal.')
|
|
221
|
-
else
|
|
222
|
-
typed_output("Could not start daemon. Run 'legionio start' manually.")
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
end
|
|
216
|
+
gaia_active = if legionio_running?
|
|
217
|
+
typed_output('GAIA is awake.')
|
|
218
|
+
sleep 0.5
|
|
219
|
+
typed_output('Heuristic mesh: nominal.')
|
|
220
|
+
sleep 0.8
|
|
221
|
+
typed_output('Cognitive threads synchronized.')
|
|
222
|
+
true
|
|
223
|
+
else
|
|
224
|
+
wake_gaia_daemon
|
|
225
|
+
end
|
|
226
226
|
|
|
227
|
+
offer_gaia_gems if gaia_active
|
|
227
228
|
@output.puts
|
|
228
229
|
end
|
|
229
|
-
|
|
230
|
+
|
|
231
|
+
def wake_gaia_daemon
|
|
232
|
+
typed_output('GAIA is dormant.')
|
|
233
|
+
sleep 1
|
|
234
|
+
@output.puts
|
|
235
|
+
return unless @wizard.confirm('Shall I wake her?')
|
|
236
|
+
|
|
237
|
+
started = start_legionio_daemon
|
|
238
|
+
if started
|
|
239
|
+
typed_output('... initializing cognitive substrate...')
|
|
240
|
+
sleep 1
|
|
241
|
+
typed_output('GAIA online. All systems nominal.')
|
|
242
|
+
else
|
|
243
|
+
typed_output("Could not start daemon. Run 'legionio start' manually.")
|
|
244
|
+
end
|
|
245
|
+
started
|
|
246
|
+
end
|
|
230
247
|
|
|
231
248
|
def collect_background_results
|
|
232
249
|
@log.log('collect', 'waiting for scanner results (10s timeout)')
|
|
@@ -392,7 +409,7 @@ module Legion
|
|
|
392
409
|
|
|
393
410
|
def store_teams_token(result)
|
|
394
411
|
require 'legion/extensions/microsoft_teams/helpers/token_cache'
|
|
395
|
-
cache = Legion::Extensions::MicrosoftTeams::Helpers::TokenCache.
|
|
412
|
+
cache = Legion::Extensions::MicrosoftTeams::Helpers::TokenCache.instance
|
|
396
413
|
cache.store_delegated_token(result)
|
|
397
414
|
cache.save_to_vault
|
|
398
415
|
rescue StandardError => e
|
|
@@ -400,6 +417,47 @@ module Legion
|
|
|
400
417
|
nil
|
|
401
418
|
end
|
|
402
419
|
|
|
420
|
+
def offer_gaia_gems
|
|
421
|
+
missing = missing_gaia_gems
|
|
422
|
+
return if missing.empty?
|
|
423
|
+
|
|
424
|
+
@output.puts
|
|
425
|
+
typed_output("#{missing.size} cognitive extension#{'s' if missing.size != 1} not installed.")
|
|
426
|
+
@output.puts
|
|
427
|
+
return unless @wizard.confirm('Install cognitive extensions?')
|
|
428
|
+
|
|
429
|
+
install_gaia_gems(missing)
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def missing_gaia_gems
|
|
433
|
+
GAIA_GEMS.reject do |gem_name|
|
|
434
|
+
Gem::Specification.find_by_name(gem_name)
|
|
435
|
+
true
|
|
436
|
+
rescue Gem::LoadError
|
|
437
|
+
false
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def install_gaia_gems(gems)
|
|
442
|
+
failed = []
|
|
443
|
+
gems.each do |gem_name|
|
|
444
|
+
typed_output(" installing #{gem_name}...")
|
|
445
|
+
@output.puts
|
|
446
|
+
Gem.install(gem_name)
|
|
447
|
+
rescue StandardError => e
|
|
448
|
+
@log.log('gaia_gems', "failed to install #{gem_name}: #{e.message}")
|
|
449
|
+
typed_output(" failed: #{gem_name}")
|
|
450
|
+
@output.puts
|
|
451
|
+
failed << gem_name
|
|
452
|
+
end
|
|
453
|
+
if failed.empty?
|
|
454
|
+
typed_output('Cognitive extensions installed.')
|
|
455
|
+
else
|
|
456
|
+
typed_output("Cognitive extensions installed with #{failed.size} failure#{'s' if failed.size != 1}.")
|
|
457
|
+
end
|
|
458
|
+
@output.puts
|
|
459
|
+
end
|
|
460
|
+
|
|
403
461
|
def detect_gem_available?
|
|
404
462
|
require 'legion/extensions/detect'
|
|
405
463
|
true
|
data/lib/legion/tty/version.rb
CHANGED