aia 1.1.1 → 2.0.0.0.pre.beta2
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/.envrc +9 -1
- data/.loki +11 -0
- data/.reek.yml +160 -0
- data/.rubocop.yml +116 -0
- data/.rubocop_strict.yml +15 -0
- data/.version +1 -1
- data/CHANGELOG.md +269 -55
- data/IMPLEMENTATION_PLAN.md +506 -0
- data/README.md +267 -239
- data/Rakefile +5 -5
- data/_typos.toml +10 -0
- data/aia.gemspec +92 -0
- data/architecture_review.md +314 -0
- data/bin/aia +16 -0
- data/config/aia.yml +13 -0
- data/docs/AGENTS.md +40 -0
- data/docs/advanced-prompting.md +67 -3
- data/docs/cli-reference.md +312 -56
- data/docs/configuration.md +130 -19
- data/docs/contributing.md +56 -2
- data/docs/directives-reference.md +593 -78
- data/docs/faq.md +85 -3
- data/docs/guides/available-models.md +1 -1
- data/docs/guides/basic-usage.md +6 -6
- data/docs/guides/chat.md +40 -16
- data/docs/guides/crew.md +239 -0
- data/docs/guides/executable-prompts.md +1 -1
- data/docs/guides/index.md +1 -0
- data/docs/guides/models.md +15 -0
- data/docs/index.md +29 -2
- data/docs/installation.md +44 -17
- data/docs/mcp-integration.md +40 -0
- data/docs/prompt_management.md +85 -86
- data/docs/security.md +47 -0
- data/docs/special_projects_guide.md +386 -0
- data/docs/tools-and-mcp-examples.md +23 -0
- data/docs/workflows-and-pipelines.md +84 -7
- data/examples/.gitignore +1 -0
- data/examples/00_setup_aia.sh +27 -44
- data/examples/11_multi_model.sh +4 -14
- data/examples/12_token_usage.sh +3 -12
- data/examples/18_tools.sh +10 -2
- data/examples/22_chat_mode.sh +0 -10
- data/examples/23_verify.sh +139 -0
- data/examples/24_decompose.sh +139 -0
- data/examples/25_spawn.sh +139 -0
- data/examples/26_debate.sh +97 -0
- data/examples/27_mention_routing.sh +157 -0
- data/examples/28_model_switching.sh +106 -0
- data/examples/29_agent_harness.sh +177 -0
- data/examples/README.md +65 -0
- data/examples/advanced_multi_robot_capabilities_without_examples.md +106 -0
- data/examples/aia_config.yml +1 -1
- data/examples/aia_config_orchestrator.yml +45 -0
- data/examples/common.sh +18 -6
- data/examples/context/tech_stack.md +2 -2
- data/examples/prompts_dir/roles/orchestrator.md +21 -0
- data/examples/requirements/sinatra_taskflow_app.md +139 -0
- data/examples/rules/01_classify_ruby.rb +16 -0
- data/examples/rules/02_prefer_claude_for_code.rb +19 -0
- data/examples/rules/03_gate_prompt_length.rb +19 -0
- data/examples/rules/04_tool_selection.rb +41 -0
- data/examples/rules/README.md +30 -0
- data/examples/run_all.sh +48 -15
- data/examples/tools/word_count_tool.rb +1 -1
- data/lib/AGENTS.md +57 -0
- data/lib/aia/chat_loop.rb +263 -167
- data/lib/aia/config/cli_parser.rb +217 -145
- data/lib/aia/config/defaults.yml +62 -33
- data/lib/aia/config/mcp_parser.rb +52 -51
- data/lib/aia/config/model_spec.rb +34 -2
- data/lib/aia/config/validator.rb +171 -216
- data/lib/aia/config.rb +111 -145
- data/lib/aia/content_extractor.rb +155 -0
- data/lib/aia/cost_calculator.rb +39 -0
- data/lib/aia/crew.rb +164 -0
- data/lib/aia/debate_handler.rb +174 -0
- data/lib/aia/delegate_handler.rb +116 -0
- data/lib/aia/directive.rb +43 -26
- data/lib/aia/directive_processor.rb +16 -7
- data/lib/aia/directives/configuration_directives.rb +214 -60
- data/lib/aia/directives/context_directives.rb +67 -52
- data/lib/aia/directives/execution_directives.rb +141 -4
- data/lib/aia/directives/model_directives.rb +163 -141
- data/lib/aia/directives/trakflow_directives.rb +62 -0
- data/lib/aia/directives/utility_directives.rb +227 -30
- data/lib/aia/directives/web_and_file_directives.rb +126 -77
- data/lib/aia/errors.rb +15 -0
- data/lib/aia/fact_asserter.rb +27 -0
- data/lib/aia/fzf.rb +9 -31
- data/lib/aia/handler_context.rb +17 -0
- data/lib/aia/handler_protocol.rb +19 -0
- data/lib/aia/history_transfer.rb +55 -0
- data/lib/aia/input_collector.rb +3 -3
- data/lib/aia/layered_orchestrator.rb +471 -0
- data/lib/aia/logger.rb +45 -25
- data/lib/aia/mcp_config_normalizer.rb +35 -0
- data/lib/aia/mcp_connection_manager.rb +315 -0
- data/lib/aia/mcp_discovery.rb +44 -0
- data/lib/aia/mcp_grouper.rb +33 -0
- data/lib/aia/mcp_server_config.rb +30 -0
- data/lib/aia/mcp_utility.rb +60 -0
- data/lib/aia/mention_router.rb +217 -0
- data/lib/aia/model_alias_registry.rb +97 -0
- data/lib/aia/model_switch_handler.rb +100 -0
- data/lib/aia/network_builder.rb +160 -0
- data/lib/aia/network_memory_manager.rb +55 -0
- data/lib/aia/patches/ruby_llm_streaming_error.rb +43 -0
- data/lib/aia/patches/ruby_llm_tool_error.rb +96 -0
- data/lib/aia/pipeline_orchestrator.rb +272 -0
- data/lib/aia/plugin_loader.rb +170 -0
- data/lib/aia/plugin_monitor.rb +211 -0
- data/lib/aia/prompt_decomposer.rb +159 -0
- data/lib/aia/prompt_handler.rb +56 -82
- data/lib/aia/robot_builder.rb +51 -0
- data/lib/aia/robot_factory.rb +338 -0
- data/lib/aia/robot_namer.rb +110 -0
- data/lib/aia/session.rb +87 -17
- data/lib/aia/session_tracker.rb +207 -0
- data/lib/aia/similarity_scorer.rb +41 -0
- data/lib/aia/skill_utils.rb +105 -1
- data/lib/aia/spawn_handler.rb +129 -0
- data/lib/aia/spawn_spec_parser.rb +65 -0
- data/lib/aia/special_mode_handler.rb +322 -0
- data/lib/aia/speech.rb +67 -0
- data/lib/aia/startup_coordinator.rb +151 -0
- data/lib/aia/streaming_runner.rb +172 -0
- data/lib/aia/system_prompt_assembler.rb +92 -0
- data/lib/aia/task_coordinator.rb +207 -0
- data/lib/aia/task_decomposer.rb +57 -0
- data/lib/aia/task_executor.rb +51 -0
- data/lib/aia/tfidf_math.rb +27 -0
- data/lib/aia/timing.rb +15 -0
- data/lib/aia/tool_filter/tfidf.rb +116 -0
- data/lib/aia/tool_filter/wordnet_expander.rb +127 -0
- data/lib/aia/tool_filter.rb +83 -0
- data/lib/aia/tool_filter_registry.rb +30 -0
- data/lib/aia/tool_filter_strategy.rb +146 -0
- data/lib/aia/tool_introspection.rb +17 -0
- data/lib/aia/tool_loader.rb +216 -0
- data/lib/aia/tool_utility.rb +30 -0
- data/lib/aia/tools/delegate_to_foreman_tool.rb +70 -0
- data/lib/aia/tools/recruit_robot_tool.rb +60 -0
- data/lib/aia/tools/reskill_robot_tool.rb +44 -0
- data/lib/aia/tools/task_board_tool.rb +115 -0
- data/lib/aia/trakflow_bridge.rb +175 -0
- data/lib/aia/turn_state.rb +95 -0
- data/lib/aia/ui_presenter.rb +182 -206
- data/lib/aia/utility.rb +136 -87
- data/lib/aia/{history_manager.rb → variable_input_collector.rb} +9 -9
- data/lib/aia/verification_network.rb +57 -0
- data/lib/aia.rb +124 -63
- data/mkdocs.yml +1 -0
- metadata +187 -58
- data/justfile +0 -215
- data/lib/aia/adapter/chat_execution.rb +0 -242
- data/lib/aia/adapter/error_handler.rb +0 -68
- data/lib/aia/adapter/gem_activator.rb +0 -57
- data/lib/aia/adapter/mcp_connector.rb +0 -274
- data/lib/aia/adapter/modality_handlers.rb +0 -167
- data/lib/aia/adapter/model_registry.rb +0 -81
- data/lib/aia/adapter/multi_model_chat.rb +0 -218
- data/lib/aia/adapter/provider_configurator.rb +0 -59
- data/lib/aia/adapter/tool_filter.rb +0 -85
- data/lib/aia/adapter/tool_loader.rb +0 -90
- data/lib/aia/chat_processor_service.rb +0 -178
- data/lib/aia/prompt_pipeline.rb +0 -183
- data/lib/aia/ruby_llm_adapter.rb +0 -95
- data/lib/extensions/openstruct_merge.rb +0 -48
- data/lib/extensions/ruby_llm/.irbrc +0 -56
- data/lib/extensions/ruby_llm/modalities.rb +0 -36
- data/lib/extensions/ruby_llm/provider_fix.rb +0 -79
- data/lib/refinements/string.rb +0 -16
- data/main.just +0 -76
|
@@ -2,93 +2,59 @@
|
|
|
2
2
|
|
|
3
3
|
module AIA
|
|
4
4
|
class ConfigurationDirectives < Directive
|
|
5
|
+
state_setting! :model
|
|
6
|
+
|
|
5
7
|
desc "View or set configuration values"
|
|
8
|
+
# :reek:TooManyStatements -- view/set dispatch on arg count, coercing booleans and dot-notation keys before writing
|
|
6
9
|
def config(args = [], context_manager = nil)
|
|
7
10
|
args = Array(args)
|
|
11
|
+
cfg = AIA.config
|
|
8
12
|
|
|
9
13
|
if args.empty?
|
|
10
|
-
ap
|
|
11
|
-
""
|
|
14
|
+
ap cfg.to_h
|
|
12
15
|
elsif args.length == 1
|
|
13
16
|
config_item = args.first
|
|
14
17
|
local_cfg = {}
|
|
15
|
-
if
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
local_cfg[config_item] = nil
|
|
19
|
-
end
|
|
18
|
+
local_cfg[config_item] = if cfg.respond_to?(config_item)
|
|
19
|
+
cfg.send(config_item)
|
|
20
|
+
end
|
|
20
21
|
ap local_cfg
|
|
21
|
-
""
|
|
22
22
|
else
|
|
23
23
|
config_item = args.shift
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
new_value = args.join(' ').gsub('=', '').strip
|
|
25
|
+
|
|
26
|
+
# Resolve the leaf key name regardless of dot-notation (flags.debug → debug)
|
|
27
|
+
leaf_key = config_item.to_s.split('.').last
|
|
26
28
|
|
|
29
|
+
boolean = AIA.respond_to?("#{leaf_key}?")
|
|
27
30
|
if boolean
|
|
28
31
|
new_value = %w[true t yes y on 1 yea yeah yep yup].include?(new_value.downcase)
|
|
29
32
|
end
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
AIA.config.send(setter, new_value)
|
|
34
|
+
if write_config_value(config_item, new_value)
|
|
35
|
+
AIA::LoggerManager.reconfigure_levels!
|
|
34
36
|
else
|
|
35
|
-
|
|
37
|
+
$stderr.puts "Warning: Unknown config option '#{config_item}'"
|
|
36
38
|
AIA::LoggerManager.aia_logger.warn("Unknown config option '#{config_item}'")
|
|
37
39
|
end
|
|
38
|
-
""
|
|
39
40
|
end
|
|
41
|
+
""
|
|
40
42
|
end
|
|
41
|
-
|
|
43
|
+
alias cfg config
|
|
42
44
|
|
|
43
45
|
desc "View or change the AI model"
|
|
44
46
|
def model(args, context_manager = nil)
|
|
47
|
+
cfg = AIA.config
|
|
48
|
+
|
|
45
49
|
if args.empty?
|
|
46
50
|
puts
|
|
47
|
-
models =
|
|
48
|
-
|
|
49
|
-
if models.size == 1
|
|
50
|
-
puts "Current Model:"
|
|
51
|
-
puts "=============="
|
|
52
|
-
puts AIA.client.model.to_h.pretty_inspect
|
|
53
|
-
else
|
|
54
|
-
puts "Multi-Model Configuration:"
|
|
55
|
-
puts "=========================="
|
|
56
|
-
puts "Model count: #{models.size}"
|
|
57
|
-
first_model = models.first.respond_to?(:name) ? models.first.name : models.first.to_s
|
|
58
|
-
puts "Primary model: #{first_model} (used for consensus when --consensus flag is enabled)"
|
|
59
|
-
consensus = AIA.config.flags.consensus
|
|
60
|
-
puts "Consensus mode: #{consensus.nil? ? 'auto-detect (disabled by default)' : consensus}"
|
|
61
|
-
puts
|
|
62
|
-
puts "Model Details:"
|
|
63
|
-
puts "-" * 50
|
|
64
|
-
|
|
65
|
-
models.each_with_index do |model_spec, index|
|
|
66
|
-
model_name = model_spec.respond_to?(:name) ? model_spec.name : model_spec.to_s
|
|
67
|
-
puts "#{index + 1}. #{model_name}#{index == 0 ? ' (primary)' : ''}"
|
|
68
|
-
|
|
69
|
-
begin
|
|
70
|
-
model_info = RubyLLM::Models.find(model_name)
|
|
71
|
-
if model_info
|
|
72
|
-
puts " Provider: #{model_info.provider || 'Unknown'}"
|
|
73
|
-
puts " Context window: #{model_info.context_window || 'Unknown'}"
|
|
74
|
-
puts " Input cost: $#{model_info.input_cost || 'Unknown'}"
|
|
75
|
-
puts " Output cost: $#{model_info.output_cost || 'Unknown'}"
|
|
76
|
-
puts " Mode: #{model_info.modalities || 'Unknown'}"
|
|
77
|
-
puts " Capabilities: #{(model_info.capabilities || []).join(', ')}" if model_info.capabilities&.any?
|
|
78
|
-
else
|
|
79
|
-
puts " Details: Model not found in registry"
|
|
80
|
-
end
|
|
81
|
-
rescue StandardError => e
|
|
82
|
-
puts " Details: Unable to fetch (#{e.class.name}: #{e.message})"
|
|
83
|
-
end
|
|
84
|
-
puts
|
|
85
|
-
end
|
|
86
|
-
end
|
|
51
|
+
models = cfg.models
|
|
52
|
+
models.size == 1 ? show_single_model(models) : show_multi_model(models)
|
|
87
53
|
puts
|
|
88
54
|
else
|
|
89
55
|
model_names = args.join(' ').split(',').map(&:strip).reject(&:empty?)
|
|
90
|
-
|
|
91
|
-
AIA.client =
|
|
56
|
+
cfg.models = AIA::Config::TO_MODEL_SPECS.call(model_names)
|
|
57
|
+
AIA.client = RobotFactory.rebuild(cfg)
|
|
92
58
|
end
|
|
93
59
|
|
|
94
60
|
''
|
|
@@ -98,12 +64,200 @@ module AIA
|
|
|
98
64
|
def temperature(args, context_manager = nil)
|
|
99
65
|
config(args.prepend('temperature'), context_manager)
|
|
100
66
|
end
|
|
101
|
-
|
|
67
|
+
alias temp temperature
|
|
102
68
|
|
|
103
69
|
desc "Set the top_p parameter for AI responses"
|
|
104
70
|
def top_p(args, context_manager = nil)
|
|
105
71
|
config(args.prepend('top_p'), context_manager)
|
|
106
72
|
end
|
|
107
|
-
|
|
73
|
+
alias topp top_p
|
|
74
|
+
|
|
75
|
+
desc "Dump session cost/token metrics as CSV"
|
|
76
|
+
# :reek:TooManyStatements -- CSV assembly: guards, header, rows, totals, print, optional file append
|
|
77
|
+
def cost(args = [], context_manager = nil)
|
|
78
|
+
tracker = AIA.session_tracker
|
|
79
|
+
unless tracker
|
|
80
|
+
puts "No session tracker available."
|
|
81
|
+
return ''
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
turns = tracker.turns.reject { |t| t[:type] == :model_switch }
|
|
85
|
+
if turns.empty?
|
|
86
|
+
puts "No turns recorded yet."
|
|
87
|
+
return ''
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
has_similarity = turns.any? { |t| t.key?(:similarity) }
|
|
91
|
+
|
|
92
|
+
header = %w[model input_tokens output_tokens total_tokens cost elapsed]
|
|
93
|
+
header << 'similarity' if has_similarity
|
|
94
|
+
lines = [header.join(',')]
|
|
95
|
+
turns.each { |turn| lines << cost_row(turn, has_similarity).join(',') }
|
|
96
|
+
lines << cost_totals_row(turns, has_similarity).join(',')
|
|
97
|
+
|
|
98
|
+
csv = lines.join("\n")
|
|
99
|
+
puts
|
|
100
|
+
puts csv
|
|
101
|
+
puts
|
|
102
|
+
|
|
103
|
+
out_file = AIA.config.output&.file
|
|
104
|
+
File.open(out_file, 'a') { |f| f.puts csv } if out_file
|
|
105
|
+
|
|
106
|
+
''
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
# One CSV row for a single recorded turn.
|
|
112
|
+
def cost_row(turn, has_similarity)
|
|
113
|
+
input_t = turn[:input_tokens] || 0
|
|
114
|
+
output_t = turn[:output_tokens] || 0
|
|
115
|
+
|
|
116
|
+
row = [
|
|
117
|
+
turn[:model],
|
|
118
|
+
input_t,
|
|
119
|
+
output_t,
|
|
120
|
+
input_t + output_t,
|
|
121
|
+
"$#{'%.5f' % (turn[:cost] || 0.0)}",
|
|
122
|
+
"%.1fs" % (turn[:elapsed] || 0)
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
if has_similarity
|
|
126
|
+
sim = turn[:similarity]
|
|
127
|
+
row << (sim.nil? ? 'ref' : "%.1f%%" % (sim * 100))
|
|
128
|
+
end
|
|
129
|
+
row
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# The TOTAL summary row across all turns.
|
|
133
|
+
# :reek:DuplicateMethodCall -- each turns.sum aggregates a different field; there is no shared value to hoist
|
|
134
|
+
def cost_totals_row(turns, has_similarity)
|
|
135
|
+
total_input = turns.sum { |t| t[:input_tokens] || 0 }
|
|
136
|
+
total_output = turns.sum { |t| t[:output_tokens] || 0 }
|
|
137
|
+
total_cost = turns.sum { |t| t[:cost] || 0.0 }
|
|
138
|
+
max_elapsed = turns.map { |t| t[:elapsed] || 0 }.max
|
|
139
|
+
|
|
140
|
+
totals = [
|
|
141
|
+
'TOTAL',
|
|
142
|
+
total_input,
|
|
143
|
+
total_output,
|
|
144
|
+
total_input + total_output,
|
|
145
|
+
"$#{'%.5f' % total_cost}",
|
|
146
|
+
"%.1fs" % max_elapsed
|
|
147
|
+
]
|
|
148
|
+
totals << '' if has_similarity
|
|
149
|
+
totals
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Format a model spec's display name whether it is a ModelSpec or a bare string.
|
|
153
|
+
def model_display_name(spec)
|
|
154
|
+
spec.respond_to?(:name) ? spec.name : spec.to_s
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def show_single_model(models)
|
|
158
|
+
puts "Current Model:"
|
|
159
|
+
puts "=============="
|
|
160
|
+
model_name = model_display_name(models.first)
|
|
161
|
+
begin
|
|
162
|
+
model_info = RubyLLM::Models.find(model_name)
|
|
163
|
+
puts model_info.to_h.pretty_inspect if model_info
|
|
164
|
+
rescue StandardError
|
|
165
|
+
puts " #{model_name}"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# :reek:TooManyStatements -- sequential terminal report: summary lines then a details block per model
|
|
170
|
+
def show_multi_model(models)
|
|
171
|
+
puts "Multi-Model Configuration:"
|
|
172
|
+
puts "=========================="
|
|
173
|
+
puts "Model count: #{models.size}"
|
|
174
|
+
puts "Primary model: #{model_display_name(models.first)} (used for consensus when --consensus flag is enabled)"
|
|
175
|
+
consensus = AIA.config.flags.consensus
|
|
176
|
+
puts "Consensus mode: #{consensus.nil? ? 'auto-detect (disabled by default)' : consensus}"
|
|
177
|
+
puts
|
|
178
|
+
puts "Model Details:"
|
|
179
|
+
puts "-" * 50
|
|
180
|
+
|
|
181
|
+
models.each_with_index do |model_spec, index|
|
|
182
|
+
puts "#{index + 1}. #{model_display_name(model_spec)}#{' (primary)' if index.zero?}"
|
|
183
|
+
show_model_details(model_display_name(model_spec))
|
|
184
|
+
puts
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def show_model_details(model_name)
|
|
189
|
+
model_info = RubyLLM::Models.find(model_name)
|
|
190
|
+
if model_info
|
|
191
|
+
puts " Provider: #{model_info.provider || 'Unknown'}"
|
|
192
|
+
puts " Context window: #{model_info.context_window || 'Unknown'}"
|
|
193
|
+
puts " Input cost: $#{model_info.input_cost || 'Unknown'}"
|
|
194
|
+
puts " Output cost: $#{model_info.output_cost || 'Unknown'}"
|
|
195
|
+
puts " Mode: #{model_info.modalities || 'Unknown'}"
|
|
196
|
+
puts " Capabilities: #{(model_info.capabilities || []).join(', ')}" if model_info.capabilities&.any?
|
|
197
|
+
else
|
|
198
|
+
puts " Details: Model not found in registry"
|
|
199
|
+
end
|
|
200
|
+
rescue StandardError => e
|
|
201
|
+
puts " Details: Unable to fetch (#{e.class.name}: #{e.message})"
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Apply a config value by key, supporting three forms:
|
|
205
|
+
# flat CLI key — "debug" → CLI_TO_NESTED_MAP routing
|
|
206
|
+
# dot-notation — "flags.debug" → navigate section.subkey
|
|
207
|
+
# direct setter — "log_level_override" → AIA.config.<key>=
|
|
208
|
+
#
|
|
209
|
+
# Returns true if the value was set, false if the key was unknown.
|
|
210
|
+
# :reek:TooManyStatements -- three key-addressing forms (dot-notation walk, CLI map, direct setter) tried in order
|
|
211
|
+
def write_config_value(config_item, new_value) # rubocop:disable Naming/PredicateMethod
|
|
212
|
+
key_str = config_item.to_s
|
|
213
|
+
cfg = AIA.config
|
|
214
|
+
|
|
215
|
+
# Dot-notation: walk the config object by path segments
|
|
216
|
+
if key_str.include?('.')
|
|
217
|
+
parts = key_str.split('.')
|
|
218
|
+
obj = cfg
|
|
219
|
+
parents = parts[0..-2]
|
|
220
|
+
leaf = parts.last
|
|
221
|
+
|
|
222
|
+
parents.each do |part|
|
|
223
|
+
obj = obj.respond_to?(part) ? obj.send(part) : nil
|
|
224
|
+
return false if obj.nil?
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
if obj.respond_to?("#{leaf}=")
|
|
228
|
+
obj.send("#{leaf}=", new_value)
|
|
229
|
+
maybe_clear_log_override(leaf, new_value)
|
|
230
|
+
return true
|
|
231
|
+
end
|
|
232
|
+
return false
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Flat CLI key via CLI_TO_NESTED_MAP (e.g. "debug" → flags.debug)
|
|
236
|
+
if AIA::Config::CLI_TO_NESTED_MAP.key?(key_str.to_sym) &&
|
|
237
|
+
cfg.respond_to?(:apply_overrides)
|
|
238
|
+
cfg.apply_overrides({ key_str.to_sym => new_value })
|
|
239
|
+
maybe_clear_log_override(key_str, new_value)
|
|
240
|
+
return true
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Direct setter on the config object (e.g. "log_level_override")
|
|
244
|
+
if cfg.respond_to?("#{key_str}=")
|
|
245
|
+
cfg.send("#{key_str}=", new_value)
|
|
246
|
+
return true
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
false
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# When debug is disabled, clear log_level_override so the logger
|
|
253
|
+
# drops back to its configured level rather than staying at DEBUG.
|
|
254
|
+
def maybe_clear_log_override(key, value)
|
|
255
|
+
return unless key.to_s == 'debug' && value == false
|
|
256
|
+
|
|
257
|
+
cfg = AIA.config
|
|
258
|
+
return unless cfg.respond_to?(:log_level_override=)
|
|
259
|
+
|
|
260
|
+
cfg.log_level_override = nil if cfg.log_level_override.to_s == 'debug'
|
|
261
|
+
end
|
|
108
262
|
end
|
|
109
263
|
end
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
module AIA
|
|
7
7
|
class ContextDirectives < Directive
|
|
8
|
+
state_setting! :clear, :checkpoint, :ckp, :cp, :restore, :review, :context
|
|
9
|
+
|
|
8
10
|
attr_accessor :checkpoint_store, :checkpoint_counter, :last_checkpoint_name
|
|
9
11
|
|
|
10
12
|
def initialize
|
|
@@ -19,6 +21,7 @@ module AIA
|
|
|
19
21
|
end
|
|
20
22
|
|
|
21
23
|
desc "Create a named checkpoint of the current context"
|
|
24
|
+
# :reek:TooManyStatements -- one snapshot build: name resolution, deep copy of every chat message list, store entry, feedback
|
|
22
25
|
def checkpoint(args, _unused = nil)
|
|
23
26
|
name = args.empty? ? nil : args.join(' ').strip
|
|
24
27
|
|
|
@@ -32,9 +35,9 @@ module AIA
|
|
|
32
35
|
|
|
33
36
|
first_chat_messages = chats.values.first&.messages || []
|
|
34
37
|
@checkpoint_store[name] = {
|
|
35
|
-
messages: chats.transform_values
|
|
38
|
+
messages: chats.transform_values do |chat|
|
|
36
39
|
chat.messages.map { |msg| deep_copy_message(msg) }
|
|
37
|
-
|
|
40
|
+
end,
|
|
38
41
|
position: first_chat_messages.size,
|
|
39
42
|
created_at: Time.now,
|
|
40
43
|
topic_preview: extract_last_user_message(first_chat_messages)
|
|
@@ -44,10 +47,11 @@ module AIA
|
|
|
44
47
|
puts "Checkpoint '#{name}' created at position #{@checkpoint_store[name][:position]}."
|
|
45
48
|
""
|
|
46
49
|
end
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
alias ckp checkpoint
|
|
51
|
+
alias cp checkpoint
|
|
49
52
|
|
|
50
53
|
desc "Restore context to a previous checkpoint"
|
|
54
|
+
# :reek:TooManyStatements -- restore flow: name resolution, validation, per-chat message replacement, user feedback
|
|
51
55
|
def restore(args, _unused = nil)
|
|
52
56
|
name = args.empty? ? nil : args.join(' ').strip
|
|
53
57
|
|
|
@@ -64,16 +68,16 @@ module AIA
|
|
|
64
68
|
end
|
|
65
69
|
|
|
66
70
|
checkpoint_data = @checkpoint_store[name]
|
|
67
|
-
|
|
71
|
+
robots = get_robots
|
|
68
72
|
|
|
69
|
-
return "Error: No active chat sessions found." if
|
|
73
|
+
return "Error: No active chat sessions found." if robots.nil? || robots.empty?
|
|
70
74
|
|
|
71
75
|
checkpoint_data[:messages].each do |model_id, saved_messages|
|
|
72
|
-
|
|
73
|
-
next unless
|
|
76
|
+
robot = robots[model_id]
|
|
77
|
+
next unless robot
|
|
74
78
|
|
|
75
79
|
restored_messages = saved_messages.map { |msg| deep_copy_message(msg) }
|
|
76
|
-
|
|
80
|
+
robot.replace_messages(restored_messages)
|
|
77
81
|
end
|
|
78
82
|
|
|
79
83
|
restored_position = checkpoint_data[:position]
|
|
@@ -82,7 +86,7 @@ module AIA
|
|
|
82
86
|
@last_checkpoint_name = name
|
|
83
87
|
|
|
84
88
|
msg = "Context restored to checkpoint '#{name}' (position #{restored_position})."
|
|
85
|
-
msg += " Removed #{removed_count} checkpoint(s) that were beyond this position." if removed_count
|
|
89
|
+
msg += " Removed #{removed_count} checkpoint(s) that were beyond this position." if removed_count.positive?
|
|
86
90
|
msg
|
|
87
91
|
end
|
|
88
92
|
|
|
@@ -90,17 +94,11 @@ module AIA
|
|
|
90
94
|
def clear(args, _unused = nil)
|
|
91
95
|
keep_system = !args.include?('--all')
|
|
92
96
|
|
|
93
|
-
|
|
94
|
-
return "Error: No active chat sessions found." if
|
|
97
|
+
robots = get_robots
|
|
98
|
+
return "Error: No active chat sessions found." if robots.nil? || robots.empty?
|
|
95
99
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
system_msg = chat.messages.find { |m| m.role == :system }
|
|
99
|
-
chat.instance_variable_set(:@messages, [])
|
|
100
|
-
chat.add_message(system_msg) if system_msg
|
|
101
|
-
else
|
|
102
|
-
chat.instance_variable_set(:@messages, [])
|
|
103
|
-
end
|
|
100
|
+
robots.each_value do |robot|
|
|
101
|
+
robot.clear_messages(keep_system: keep_system)
|
|
104
102
|
end
|
|
105
103
|
|
|
106
104
|
@checkpoint_store.clear
|
|
@@ -115,39 +113,20 @@ module AIA
|
|
|
115
113
|
chats = get_chats
|
|
116
114
|
return "Error: No active chat sessions found." if chats.nil? || chats.empty?
|
|
117
115
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
puts "\n=== Chat Context (RubyLLM) ==="
|
|
122
|
-
puts "Total messages: #{messages.size}"
|
|
123
|
-
puts "Models: #{chats.keys.join(', ')}"
|
|
124
|
-
puts "Checkpoints: #{checkpoint_names.join(', ')}" if checkpoint_names.any?
|
|
125
|
-
puts
|
|
116
|
+
messages = chats.values.first&.messages || []
|
|
117
|
+
print_review_header(chats, messages.size)
|
|
126
118
|
|
|
127
119
|
positions = checkpoint_positions
|
|
128
|
-
|
|
129
120
|
messages.each_with_index do |msg, index|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
puts "-" * 40
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
role = msg.role.to_s.capitalize
|
|
136
|
-
content = format_message_content(msg)
|
|
137
|
-
|
|
138
|
-
puts "#{index + 1}. [#{role}]: #{content}"
|
|
139
|
-
puts
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
if positions[messages.size]
|
|
143
|
-
puts "📍 [Checkpoint: #{positions[messages.size].join(', ')}]"
|
|
144
|
-
puts "-" * 40
|
|
121
|
+
print_checkpoint_marker(positions[index])
|
|
122
|
+
print_review_message(msg, index)
|
|
145
123
|
end
|
|
124
|
+
print_checkpoint_marker(positions[messages.size])
|
|
146
125
|
|
|
147
126
|
puts "=== End of Context ==="
|
|
148
127
|
""
|
|
149
128
|
end
|
|
150
|
-
|
|
129
|
+
alias context review
|
|
151
130
|
|
|
152
131
|
desc "List all available checkpoints"
|
|
153
132
|
def checkpoints_list(args, _unused = nil)
|
|
@@ -159,15 +138,16 @@ module AIA
|
|
|
159
138
|
puts "\n=== Available Checkpoints ==="
|
|
160
139
|
@checkpoint_store.each do |name, data|
|
|
161
140
|
created = data[:created_at]&.strftime('%H:%M:%S') || 'unknown'
|
|
141
|
+
preview = data[:topic_preview]
|
|
162
142
|
puts " #{name}: position #{data[:position]}, created #{created}"
|
|
163
|
-
if
|
|
164
|
-
puts " → \"#{
|
|
143
|
+
if preview && !preview.empty?
|
|
144
|
+
puts " → \"#{preview}\""
|
|
165
145
|
end
|
|
166
146
|
end
|
|
167
147
|
puts "=== End of Checkpoints ==="
|
|
168
148
|
""
|
|
169
149
|
end
|
|
170
|
-
|
|
150
|
+
alias checkpoints checkpoints_list
|
|
171
151
|
|
|
172
152
|
# --- helpers (no desc → not registered as directives) ---
|
|
173
153
|
|
|
@@ -175,6 +155,27 @@ module AIA
|
|
|
175
155
|
@checkpoint_store.keys
|
|
176
156
|
end
|
|
177
157
|
|
|
158
|
+
def print_review_header(chats, total)
|
|
159
|
+
puts "\n=== Chat Context (RubyLLM) ==="
|
|
160
|
+
puts "Total messages: #{total}"
|
|
161
|
+
puts "Models: #{chats.keys.join(', ')}"
|
|
162
|
+
puts "Checkpoints: #{checkpoint_names.join(', ')}" if checkpoint_names.any?
|
|
163
|
+
puts
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Print the marker block when checkpoint names exist at this position.
|
|
167
|
+
def print_checkpoint_marker(names)
|
|
168
|
+
return unless names
|
|
169
|
+
|
|
170
|
+
puts "📍 [Checkpoint: #{names.join(', ')}]"
|
|
171
|
+
puts "-" * 40
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def print_review_message(msg, index)
|
|
175
|
+
puts "#{index + 1}. [#{msg.role.to_s.capitalize}]: #{format_message_content(msg)}"
|
|
176
|
+
puts
|
|
177
|
+
end
|
|
178
|
+
|
|
178
179
|
def checkpoint_positions
|
|
179
180
|
positions = {}
|
|
180
181
|
@checkpoint_store.each do |name, data|
|
|
@@ -200,9 +201,23 @@ module AIA
|
|
|
200
201
|
|
|
201
202
|
private
|
|
202
203
|
|
|
204
|
+
# In v2, AIA.client is a RobotLab::Robot which wraps a RubyLLM::Chat.
|
|
205
|
+
# Returns a hash of { name => Robot } for checkpoint operations.
|
|
206
|
+
def get_robots
|
|
207
|
+
robot = AIA.client
|
|
208
|
+
return nil unless robot
|
|
209
|
+
|
|
210
|
+
name = robot.respond_to?(:name) ? robot.name : "default"
|
|
211
|
+
{ name => robot }
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Return { name => RubyLLM::Chat } for operations that need direct chat access.
|
|
215
|
+
# Uses Robot#chat public API.
|
|
203
216
|
def get_chats
|
|
204
|
-
|
|
205
|
-
|
|
217
|
+
robots = get_robots
|
|
218
|
+
return nil unless robots
|
|
219
|
+
|
|
220
|
+
robots.transform_values(&:chat)
|
|
206
221
|
end
|
|
207
222
|
|
|
208
223
|
def deep_copy_message(msg)
|
|
@@ -233,12 +248,12 @@ module AIA
|
|
|
233
248
|
def extract_last_user_message(messages, max_length: 70)
|
|
234
249
|
return "" if messages.nil? || messages.empty?
|
|
235
250
|
|
|
236
|
-
last_user_msg = messages.
|
|
251
|
+
last_user_msg = messages.rfind { |msg| msg.role == :user }
|
|
237
252
|
return "" unless last_user_msg
|
|
238
253
|
|
|
239
254
|
content = last_user_msg.content.to_s.strip
|
|
240
255
|
content = content.gsub(/\s+/, ' ')
|
|
241
|
-
content.length > max_length ? "#{content[0..max_length - 4]}..." : content
|
|
256
|
+
content.length > max_length ? "#{content[0..(max_length - 4)]}..." : content
|
|
242
257
|
end
|
|
243
258
|
end
|
|
244
259
|
end
|