legionio 1.9.31 → 1.9.34
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 +31 -0
- data/Gemfile +0 -1
- data/lib/legion/api/extensions.rb +33 -2
- data/lib/legion/cli/chat/extension_tool_loader.rb +1 -1
- data/lib/legion/cli/chat/permissions.rb +6 -7
- data/lib/legion/cli/chat/tool_registry.rb +83 -93
- data/lib/legion/cli/chat/tools/arbitrage_status.rb +13 -12
- data/lib/legion/cli/chat/tools/budget_status.rb +20 -19
- data/lib/legion/cli/chat/tools/consolidate_memory.rb +14 -11
- data/lib/legion/cli/chat/tools/cost_summary.rb +18 -16
- data/lib/legion/cli/chat/tools/detect_anomalies.rb +13 -11
- data/lib/legion/cli/chat/tools/edit_file.rb +16 -12
- data/lib/legion/cli/chat/tools/entity_extract.rb +18 -24
- data/lib/legion/cli/chat/tools/escalation_status.rb +13 -12
- data/lib/legion/cli/chat/tools/generate_insights.rb +23 -24
- data/lib/legion/cli/chat/tools/graph_explore.rb +17 -19
- data/lib/legion/cli/chat/tools/ingest_knowledge.rb +17 -14
- data/lib/legion/cli/chat/tools/knowledge_maintenance.rb +16 -12
- data/lib/legion/cli/chat/tools/knowledge_stats.rb +8 -9
- data/lib/legion/cli/chat/tools/list_extensions.rb +17 -15
- data/lib/legion/cli/chat/tools/manage_schedules.rb +21 -19
- data/lib/legion/cli/chat/tools/manage_tasks.rb +27 -36
- data/lib/legion/cli/chat/tools/memory_status.rb +23 -23
- data/lib/legion/cli/chat/tools/model_comparison.rb +18 -21
- data/lib/legion/cli/chat/tools/provider_health.rb +22 -19
- data/lib/legion/cli/chat/tools/query_knowledge.rb +15 -11
- data/lib/legion/cli/chat/tools/read_file.rb +12 -6
- data/lib/legion/cli/chat/tools/reflect.rb +19 -16
- data/lib/legion/cli/chat/tools/relate_knowledge.rb +16 -12
- data/lib/legion/cli/chat/tools/run_command.rb +17 -13
- data/lib/legion/cli/chat/tools/save_memory.rb +13 -10
- data/lib/legion/cli/chat/tools/scheduling_status.rb +16 -15
- data/lib/legion/cli/chat/tools/search_content.rb +12 -6
- data/lib/legion/cli/chat/tools/search_files.rb +11 -5
- data/lib/legion/cli/chat/tools/search_memory.rb +12 -8
- data/lib/legion/cli/chat/tools/search_traces.rb +31 -27
- data/lib/legion/cli/chat/tools/shadow_eval_status.rb +15 -14
- data/lib/legion/cli/chat/tools/spawn_agent.rb +12 -8
- data/lib/legion/cli/chat/tools/summarize_traces.rb +14 -11
- data/lib/legion/cli/chat/tools/system_status.rb +10 -11
- data/lib/legion/cli/chat/tools/trigger_dream.rb +19 -17
- data/lib/legion/cli/chat/tools/view_events.rb +14 -12
- data/lib/legion/cli/chat/tools/view_trends.rb +18 -18
- data/lib/legion/cli/chat/tools/web_search.rb +11 -5
- data/lib/legion/cli/chat/tools/worker_status.rb +19 -18
- data/lib/legion/cli/chat/tools/write_file.rb +11 -5
- data/lib/legion/cli/generate_command.rb +13 -6
- data/lib/legion/extensions/absorbers/base.rb +3 -1
- data/lib/legion/extensions/core.rb +2 -1
- data/lib/legion/extensions.rb +1 -1
- data/lib/legion/identity/process.rb +50 -36
- data/lib/legion/tools/discovery.rb +23 -0
- 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: e0fb2242e4fffe902c79cac15713bb57d8b2dd6388f122a9a6f14271faa3b3b2
|
|
4
|
+
data.tar.gz: c70d462ff6369c61dd5ae9bd786b325e4e53789eddc78d10d97441793eec1360
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d7f43d17ba3a681ea09e82678831ba5694a50af90d508eb27acdcc6743d47c74bfec7da597b6d2fe6805e968d8ba247be571f6e26e60d1cfa86d6814974eb9ec
|
|
7
|
+
data.tar.gz: 198d3a7b98bb187f23f0e5cfedacabd41983dd52b0ae238650197d2ce9a4888016d211123579cd287e5a2b05f496b0dba6fb627b6280124cf39ca92b11917a18
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.9.34] - 2026-05-18
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- API: `GET /api/extensions/tools` endpoint with extension, runner, deferred, and triggered filters
|
|
9
|
+
- Tools::Discovery: writes to `Legion::Settings::Extensions.register_tool` (bridges discovery to LLM pipeline)
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- Extensions: `extension_parts_from_const` no longer converts underscores to dashes (fixes lex-microsoft_teams filtering)
|
|
13
|
+
- Core: `generate_runner_messages` strips `?` and `!` from method names before creating constants
|
|
14
|
+
|
|
15
|
+
## [1.9.33] - 2026-05-15
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- `Legion::Identity::Process` stores and exposes `db_principal_id` and `db_identity_id` integer PKs — present in `EMPTY_STATE`, persisted through `bind!`, and included in `identity_hash`. Both default to nil until an identity provider populates them.
|
|
19
|
+
|
|
20
|
+
## [1.9.32] - 2026-05-14
|
|
21
|
+
|
|
22
|
+
### Removed
|
|
23
|
+
- Removed `gem 'ruby_llm'` dependency from Gemfile; all 40 CLI chat tools now use `Legion::Tools::Base` natively.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Migrated all 40 CLI chat tool classes from `RubyLLM::Tool` to `Legion::Tools::Base`:
|
|
27
|
+
- `param` DSL replaced with `input_schema` (JSON Schema hash)
|
|
28
|
+
- `def execute` instance method replaced with `def self.call` class method
|
|
29
|
+
- Explicit `tool_name 'legion.<snake_case>'` added to each tool
|
|
30
|
+
- Private instance helpers converted to class methods
|
|
31
|
+
- Updated `tool_registry.rb`: removed `require 'ruby_llm'` and `begin/rescue LoadError` guard.
|
|
32
|
+
- Updated `extension_tool_loader.rb`: `klass < RubyLLM::Tool` changed to `klass < Legion::Tools::Base`.
|
|
33
|
+
- Updated `generate_command.rb` tool template to emit `Legion::Tools::Base` with `input_schema` and `def self.call`.
|
|
34
|
+
- `Permissions::Gate` now prepends on the singleton class to intercept `self.call` correctly.
|
|
35
|
+
|
|
5
36
|
## [1.9.31] - 2026-05-14
|
|
6
37
|
|
|
7
38
|
### Added
|
data/Gemfile
CHANGED
|
@@ -6,6 +6,7 @@ module Legion
|
|
|
6
6
|
module Extensions
|
|
7
7
|
def self.registered(app)
|
|
8
8
|
register_loaded_summary_route(app)
|
|
9
|
+
register_tools_route(app)
|
|
9
10
|
register_available_route(app)
|
|
10
11
|
register_extension_routes(app)
|
|
11
12
|
register_runner_routes(app)
|
|
@@ -29,6 +30,14 @@ module Legion
|
|
|
29
30
|
end
|
|
30
31
|
end
|
|
31
32
|
|
|
33
|
+
def self.register_tools_route(app)
|
|
34
|
+
app.get '/api/extensions/tools' do
|
|
35
|
+
entries = filter_tool_entries(Array(Legion::Settings::Extensions.tools), params)
|
|
36
|
+
tools = entries.map { |e| serialize_tool_entry(e) }
|
|
37
|
+
json_response({ total: tools.size, tools: tools })
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
32
41
|
def self.register_available_route(app)
|
|
33
42
|
app.get '/api/extension_catalog/available' do
|
|
34
43
|
entries = Legion::Extensions::Catalog::Available.all
|
|
@@ -205,8 +214,30 @@ module Legion
|
|
|
205
214
|
started_at: entry[:started_at]&.iso8601 }
|
|
206
215
|
end
|
|
207
216
|
|
|
208
|
-
|
|
209
|
-
|
|
217
|
+
def filter_tool_entries(entries, params)
|
|
218
|
+
entries = entries.select { |e| e[:extension].to_s == params[:extension] } if params[:extension]
|
|
219
|
+
entries = entries.select { |e| e[:runner].to_s == params[:runner] } if params[:runner]
|
|
220
|
+
entries = entries.select { |e| e[:deferred] == (params[:deferred] == 'true') } if params.key?(:deferred)
|
|
221
|
+
entries = entries.select { |e| Array(e[:trigger_words]).any? } if params[:triggered] == 'true'
|
|
222
|
+
entries
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def serialize_tool_entry(entry)
|
|
226
|
+
{
|
|
227
|
+
name: entry[:name],
|
|
228
|
+
description: entry[:description],
|
|
229
|
+
extension: entry[:extension],
|
|
230
|
+
runner: entry[:runner],
|
|
231
|
+
deferred: entry[:deferred],
|
|
232
|
+
trigger_words: entry[:trigger_words],
|
|
233
|
+
source: entry[:source],
|
|
234
|
+
sticky: entry[:sticky]
|
|
235
|
+
}.compact
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
private :register_loaded_summary_route, :register_tools_route, :register_available_route,
|
|
239
|
+
:register_extension_routes, :register_runner_routes, :register_function_routes,
|
|
240
|
+
:register_invoke_route
|
|
210
241
|
end
|
|
211
242
|
end
|
|
212
243
|
end
|
|
@@ -24,7 +24,7 @@ module Legion
|
|
|
24
24
|
def collect_tool_classes(tools_module)
|
|
25
25
|
tools_module.constants.filter_map do |const_name|
|
|
26
26
|
klass = tools_module.const_get(const_name)
|
|
27
|
-
klass if klass.is_a?(Class) && klass <
|
|
27
|
+
klass if klass.is_a?(Class) && klass < Legion::Tools::Base
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
@@ -58,16 +58,15 @@ module Legion
|
|
|
58
58
|
def apply!(tool_classes)
|
|
59
59
|
tool_classes.each do |klass|
|
|
60
60
|
tier = tier_for(klass)
|
|
61
|
-
klass.prepend(Gate) unless tier == :read
|
|
61
|
+
klass.singleton_class.prepend(Gate) unless tier == :read
|
|
62
62
|
end
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
module Gate
|
|
67
|
-
def call(args)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return 'Tool execution denied by user.' unless Permissions.confirm?(desc)
|
|
67
|
+
def call(**args)
|
|
68
|
+
desc = permission_description(args)
|
|
69
|
+
return error_response('Tool execution denied by user.') unless Permissions.confirm?(desc)
|
|
71
70
|
|
|
72
71
|
super
|
|
73
72
|
end
|
|
@@ -75,11 +74,11 @@ module Legion
|
|
|
75
74
|
private
|
|
76
75
|
|
|
77
76
|
def permission_description(args)
|
|
78
|
-
tier = Permissions.tier_for(self
|
|
77
|
+
tier = Permissions.tier_for(self)
|
|
79
78
|
case tier
|
|
80
79
|
when :write
|
|
81
80
|
path = args[:path] || '(unknown)'
|
|
82
|
-
action =
|
|
81
|
+
action = name.split('::').last.gsub(/([a-z])([A-Z])/, '\1 \2')
|
|
83
82
|
"#{action}: #{path}"
|
|
84
83
|
when :shell
|
|
85
84
|
"Run command: #{args[:command]}"
|
|
@@ -2,52 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
require 'legion/cli/chat_command'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
require 'legion/cli/chat/tools/graph_explore'
|
|
46
|
-
require 'legion/cli/chat/tools/scheduling_status'
|
|
47
|
-
require 'legion/cli/chat/tools/memory_status'
|
|
48
|
-
rescue LoadError => e
|
|
49
|
-
Legion::Logging.debug("ToolRegistry ruby_llm not available, chat tools will not be registered: #{e.message}") if defined?(Legion::Logging)
|
|
50
|
-
end
|
|
5
|
+
require 'legion/cli/chat/tools/read_file'
|
|
6
|
+
require 'legion/cli/chat/tools/write_file'
|
|
7
|
+
require 'legion/cli/chat/tools/edit_file'
|
|
8
|
+
require 'legion/cli/chat/tools/search_files'
|
|
9
|
+
require 'legion/cli/chat/tools/search_content'
|
|
10
|
+
require 'legion/cli/chat/tools/run_command'
|
|
11
|
+
require 'legion/cli/chat/tools/save_memory'
|
|
12
|
+
require 'legion/cli/chat/tools/search_memory'
|
|
13
|
+
require 'legion/cli/chat/tools/web_search'
|
|
14
|
+
require 'legion/cli/chat/tools/spawn_agent'
|
|
15
|
+
require 'legion/cli/chat/tools/search_traces'
|
|
16
|
+
require 'legion/cli/chat/tools/query_knowledge'
|
|
17
|
+
require 'legion/cli/chat/tools/ingest_knowledge'
|
|
18
|
+
require 'legion/cli/chat/tools/consolidate_memory'
|
|
19
|
+
require 'legion/cli/chat/tools/relate_knowledge'
|
|
20
|
+
require 'legion/cli/chat/tools/knowledge_maintenance'
|
|
21
|
+
require 'legion/cli/chat/tools/knowledge_stats'
|
|
22
|
+
require 'legion/cli/chat/tools/summarize_traces'
|
|
23
|
+
require 'legion/cli/chat/tools/list_extensions'
|
|
24
|
+
require 'legion/cli/chat/tools/manage_tasks'
|
|
25
|
+
require 'legion/cli/chat/tools/system_status'
|
|
26
|
+
require 'legion/cli/chat/tools/view_events'
|
|
27
|
+
require 'legion/cli/chat/tools/cost_summary'
|
|
28
|
+
require 'legion/cli/chat/tools/reflect'
|
|
29
|
+
require 'legion/cli/chat/tools/manage_schedules'
|
|
30
|
+
require 'legion/cli/chat/tools/worker_status'
|
|
31
|
+
require 'legion/cli/chat/tools/detect_anomalies'
|
|
32
|
+
require 'legion/cli/chat/tools/view_trends'
|
|
33
|
+
require 'legion/cli/chat/tools/trigger_dream'
|
|
34
|
+
require 'legion/cli/chat/tools/generate_insights'
|
|
35
|
+
require 'legion/cli/chat/tools/budget_status'
|
|
36
|
+
require 'legion/cli/chat/tools/provider_health'
|
|
37
|
+
require 'legion/cli/chat/tools/model_comparison'
|
|
38
|
+
require 'legion/cli/chat/tools/shadow_eval_status'
|
|
39
|
+
require 'legion/cli/chat/tools/entity_extract'
|
|
40
|
+
require 'legion/cli/chat/tools/arbitrage_status'
|
|
41
|
+
require 'legion/cli/chat/tools/escalation_status'
|
|
42
|
+
require 'legion/cli/chat/tools/graph_explore'
|
|
43
|
+
require 'legion/cli/chat/tools/scheduling_status'
|
|
44
|
+
require 'legion/cli/chat/tools/memory_status'
|
|
51
45
|
|
|
52
46
|
require 'legion/cli/chat/permissions'
|
|
53
47
|
|
|
@@ -55,54 +49,50 @@ module Legion
|
|
|
55
49
|
module CLI
|
|
56
50
|
class Chat
|
|
57
51
|
module ToolRegistry
|
|
58
|
-
BUILTIN_TOOLS =
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
].freeze
|
|
101
|
-
else
|
|
102
|
-
[].freeze
|
|
103
|
-
end
|
|
52
|
+
BUILTIN_TOOLS = [
|
|
53
|
+
Tools::ReadFile,
|
|
54
|
+
Tools::WriteFile,
|
|
55
|
+
Tools::EditFile,
|
|
56
|
+
Tools::SearchFiles,
|
|
57
|
+
Tools::SearchContent,
|
|
58
|
+
Tools::RunCommand,
|
|
59
|
+
Tools::SaveMemory,
|
|
60
|
+
Tools::SearchMemory,
|
|
61
|
+
Tools::WebSearch,
|
|
62
|
+
Tools::SpawnAgent,
|
|
63
|
+
Tools::SearchTraces,
|
|
64
|
+
Tools::QueryKnowledge,
|
|
65
|
+
Tools::IngestKnowledge,
|
|
66
|
+
Tools::ConsolidateMemory,
|
|
67
|
+
Tools::RelateKnowledge,
|
|
68
|
+
Tools::KnowledgeMaintenance,
|
|
69
|
+
Tools::KnowledgeStats,
|
|
70
|
+
Tools::SummarizeTraces,
|
|
71
|
+
Tools::ListExtensions,
|
|
72
|
+
Tools::ManageTasks,
|
|
73
|
+
Tools::SystemStatus,
|
|
74
|
+
Tools::ViewEvents,
|
|
75
|
+
Tools::CostSummary,
|
|
76
|
+
Tools::Reflect,
|
|
77
|
+
Tools::ManageSchedules,
|
|
78
|
+
Tools::WorkerStatus,
|
|
79
|
+
Tools::DetectAnomalies,
|
|
80
|
+
Tools::ViewTrends,
|
|
81
|
+
Tools::TriggerDream,
|
|
82
|
+
Tools::GenerateInsights,
|
|
83
|
+
Tools::BudgetStatus,
|
|
84
|
+
Tools::ProviderHealth,
|
|
85
|
+
Tools::ModelComparison,
|
|
86
|
+
Tools::ShadowEvalStatus,
|
|
87
|
+
Tools::EntityExtract,
|
|
88
|
+
Tools::ArbitrageStatus,
|
|
89
|
+
Tools::EscalationStatus,
|
|
90
|
+
Tools::GraphExplore,
|
|
91
|
+
Tools::SchedulingStatus,
|
|
92
|
+
Tools::MemoryStatus
|
|
93
|
+
].freeze
|
|
104
94
|
|
|
105
|
-
Permissions.apply!(BUILTIN_TOOLS)
|
|
95
|
+
Permissions.apply!(BUILTIN_TOOLS)
|
|
106
96
|
|
|
107
97
|
def self.builtin_tools
|
|
108
98
|
BUILTIN_TOOLS.dup
|
|
@@ -4,17 +4,20 @@ module Legion
|
|
|
4
4
|
module CLI
|
|
5
5
|
class Chat
|
|
6
6
|
module Tools
|
|
7
|
-
class ArbitrageStatus <
|
|
7
|
+
class ArbitrageStatus < Legion::Tools::Base
|
|
8
|
+
tool_name 'legion.arbitrage_status'
|
|
8
9
|
description 'Show LLM cost arbitrage status: model pricing table, cheapest model per capability tier'
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
input_schema({
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
capability: { type: 'string', description: 'Capability tier to check: basic, moderate, or reasoning (default: show all)' }
|
|
14
|
+
},
|
|
15
|
+
required: []
|
|
16
|
+
})
|
|
14
17
|
|
|
15
18
|
TIERS = %i[basic moderate reasoning].freeze
|
|
16
19
|
|
|
17
|
-
def
|
|
20
|
+
def self.call(capability: nil)
|
|
18
21
|
return 'LLM arbitrage module not available.' unless arbitrage_available?
|
|
19
22
|
|
|
20
23
|
if capability
|
|
@@ -24,13 +27,11 @@ module Legion
|
|
|
24
27
|
end
|
|
25
28
|
end
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def arbitrage_available?
|
|
30
|
+
def self.arbitrage_available?
|
|
30
31
|
defined?(Legion::LLM::Arbitrage)
|
|
31
32
|
end
|
|
32
33
|
|
|
33
|
-
def format_overview
|
|
34
|
+
def self.format_overview
|
|
34
35
|
arb = Legion::LLM::Arbitrage
|
|
35
36
|
lines = ["LLM Cost Arbitrage\n"]
|
|
36
37
|
lines << format(' Enabled: %<v>s', v: arb.enabled? ? 'YES' : 'no')
|
|
@@ -56,7 +57,7 @@ module Legion
|
|
|
56
57
|
lines.join("\n")
|
|
57
58
|
end
|
|
58
59
|
|
|
59
|
-
def format_tier(tier)
|
|
60
|
+
def self.format_tier(tier)
|
|
60
61
|
arb = Legion::LLM::Arbitrage
|
|
61
62
|
return format('Invalid tier: %<t>s. Use: %<valid>s', t: tier, valid: TIERS.join(', ')) unless TIERS.include?(tier)
|
|
62
63
|
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'ruby_llm'
|
|
4
|
-
|
|
5
3
|
begin
|
|
6
4
|
require 'legion/cli/chat_command'
|
|
7
5
|
rescue LoadError
|
|
@@ -12,15 +10,20 @@ module Legion
|
|
|
12
10
|
module CLI
|
|
13
11
|
class Chat
|
|
14
12
|
module Tools
|
|
15
|
-
class BudgetStatus <
|
|
13
|
+
class BudgetStatus < Legion::Tools::Base
|
|
14
|
+
tool_name 'legion.budget_status'
|
|
16
15
|
description 'Check the current LLM session cost budget status. Shows how much has been spent, ' \
|
|
17
16
|
'remaining budget, and whether the budget guard is enforcing limits. Works locally ' \
|
|
18
17
|
'without needing the Legion daemon. Use this when the user asks about spending or budget.'
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
input_schema({
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: {
|
|
21
|
+
action: { type: 'string', description: 'Action: "status" (default), "summary" (cost breakdown by model)' }
|
|
22
|
+
},
|
|
23
|
+
required: []
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
def self.call(action: 'status')
|
|
24
27
|
return 'Legion::LLM not available.' unless llm_available?
|
|
25
28
|
|
|
26
29
|
case action.to_s
|
|
@@ -32,9 +35,7 @@ module Legion
|
|
|
32
35
|
"Error checking budget: #{e.message}"
|
|
33
36
|
end
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def format_status
|
|
38
|
+
def self.format_status
|
|
38
39
|
guard = budget_guard_status
|
|
39
40
|
tracker = cost_summary
|
|
40
41
|
lines = ["Session Budget Status:\n"]
|
|
@@ -49,7 +50,7 @@ module Legion
|
|
|
49
50
|
lines.join("\n")
|
|
50
51
|
end
|
|
51
52
|
|
|
52
|
-
def format_summary
|
|
53
|
+
def self.format_summary
|
|
53
54
|
tracker = cost_summary
|
|
54
55
|
return 'No LLM requests recorded this session.' if tracker[:total_requests].zero?
|
|
55
56
|
|
|
@@ -63,7 +64,7 @@ module Legion
|
|
|
63
64
|
lines.join("\n")
|
|
64
65
|
end
|
|
65
66
|
|
|
66
|
-
def append_model_breakdown(lines, by_model)
|
|
67
|
+
def self.append_model_breakdown(lines, by_model)
|
|
67
68
|
return unless by_model&.any?
|
|
68
69
|
|
|
69
70
|
lines << "\n By Model:"
|
|
@@ -73,31 +74,31 @@ module Legion
|
|
|
73
74
|
end
|
|
74
75
|
end
|
|
75
76
|
|
|
76
|
-
def budget_guard_status
|
|
77
|
+
def self.budget_guard_status
|
|
77
78
|
return { enforcing: false, budget_usd: 0.0, ratio: 0.0 } unless budget_guard_available?
|
|
78
79
|
|
|
79
80
|
Legion::LLM::Hooks::BudgetGuard.status
|
|
80
81
|
end
|
|
81
82
|
|
|
82
|
-
def cost_summary
|
|
83
|
+
def self.cost_summary
|
|
83
84
|
return empty_summary unless cost_tracker_available?
|
|
84
85
|
|
|
85
86
|
Legion::LLM::CostTracker.summary
|
|
86
87
|
end
|
|
87
88
|
|
|
88
|
-
def budget_guard_available?
|
|
89
|
+
def self.budget_guard_available?
|
|
89
90
|
defined?(Legion::LLM::Hooks::BudgetGuard)
|
|
90
91
|
end
|
|
91
92
|
|
|
92
|
-
def cost_tracker_available?
|
|
93
|
+
def self.cost_tracker_available?
|
|
93
94
|
defined?(Legion::LLM::CostTracker)
|
|
94
95
|
end
|
|
95
96
|
|
|
96
|
-
def llm_available?
|
|
97
|
+
def self.llm_available?
|
|
97
98
|
defined?(Legion::LLM)
|
|
98
99
|
end
|
|
99
100
|
|
|
100
|
-
def empty_summary
|
|
101
|
+
def self.empty_summary
|
|
101
102
|
{ total_cost_usd: 0.0, total_requests: 0, total_input_tokens: 0, total_output_tokens: 0, by_model: {} }
|
|
102
103
|
end
|
|
103
104
|
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'ruby_llm'
|
|
4
|
-
|
|
5
3
|
begin
|
|
6
4
|
require 'legion/cli/chat_command'
|
|
7
5
|
require 'legion/cli/chat/memory_store'
|
|
@@ -13,12 +11,19 @@ module Legion
|
|
|
13
11
|
module CLI
|
|
14
12
|
class Chat
|
|
15
13
|
module Tools
|
|
16
|
-
class ConsolidateMemory <
|
|
14
|
+
class ConsolidateMemory < Legion::Tools::Base
|
|
15
|
+
tool_name 'legion.consolidate_memory'
|
|
17
16
|
description 'Consolidate and organize memory entries by removing duplicates, merging related items, ' \
|
|
18
17
|
'and creating cleaner summaries. Use this when memory has grown cluttered or has redundant entries. ' \
|
|
19
18
|
'Pass scope "project" or "global" to target the right memory file.'
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
input_schema({
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
scope: { type: 'string', description: 'Memory scope: "project" or "global" (default: project)' },
|
|
23
|
+
dry_run: { type: 'string', description: 'Set to "true" to preview without writing (default: false)' }
|
|
24
|
+
},
|
|
25
|
+
required: []
|
|
26
|
+
})
|
|
22
27
|
|
|
23
28
|
CONSOLIDATION_PROMPT = <<~PROMPT
|
|
24
29
|
You are a memory consolidation engine. Given a list of memory entries, produce a cleaned-up version that:
|
|
@@ -34,7 +39,7 @@ module Legion
|
|
|
34
39
|
Do NOT add headers, explanations, or commentary.
|
|
35
40
|
PROMPT
|
|
36
41
|
|
|
37
|
-
def
|
|
42
|
+
def self.call(scope: 'project', dry_run: nil)
|
|
38
43
|
dry_run = dry_run.to_s == 'true'
|
|
39
44
|
scope_sym = scope.to_s == 'global' ? :global : :project
|
|
40
45
|
|
|
@@ -60,9 +65,7 @@ module Legion
|
|
|
60
65
|
"Error consolidating memory: #{e.message}"
|
|
61
66
|
end
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def consolidate_entries(entries)
|
|
68
|
+
def self.consolidate_entries(entries)
|
|
66
69
|
return nil unless defined?(Legion::LLM) && Legion::LLM.respond_to?(:chat_direct)
|
|
67
70
|
|
|
68
71
|
numbered = entries.map.with_index(1) { |e, i| "#{i}. #{e}" }.join("\n")
|
|
@@ -72,7 +75,7 @@ module Legion
|
|
|
72
75
|
response.content
|
|
73
76
|
end
|
|
74
77
|
|
|
75
|
-
def parse_consolidated(text)
|
|
78
|
+
def self.parse_consolidated(text)
|
|
76
79
|
text.lines
|
|
77
80
|
.map(&:strip)
|
|
78
81
|
.select { |line| line.start_with?('- ') }
|
|
@@ -80,7 +83,7 @@ module Legion
|
|
|
80
83
|
.reject(&:empty?)
|
|
81
84
|
end
|
|
82
85
|
|
|
83
|
-
def write_consolidated(entries, scope_sym)
|
|
86
|
+
def self.write_consolidated(entries, scope_sym)
|
|
84
87
|
path = scope_sym == :global ? MemoryStore.global_path : MemoryStore.project_path
|
|
85
88
|
header = scope_sym == :global ? "# Global Memory\n" : "# Project Memory\n"
|
|
86
89
|
timestamp = Time.now.strftime('%Y-%m-%d %H:%M')
|