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.
Files changed (175) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +9 -1
  3. data/.loki +11 -0
  4. data/.reek.yml +160 -0
  5. data/.rubocop.yml +116 -0
  6. data/.rubocop_strict.yml +15 -0
  7. data/.version +1 -1
  8. data/CHANGELOG.md +269 -55
  9. data/IMPLEMENTATION_PLAN.md +506 -0
  10. data/README.md +267 -239
  11. data/Rakefile +5 -5
  12. data/_typos.toml +10 -0
  13. data/aia.gemspec +92 -0
  14. data/architecture_review.md +314 -0
  15. data/bin/aia +16 -0
  16. data/config/aia.yml +13 -0
  17. data/docs/AGENTS.md +40 -0
  18. data/docs/advanced-prompting.md +67 -3
  19. data/docs/cli-reference.md +312 -56
  20. data/docs/configuration.md +130 -19
  21. data/docs/contributing.md +56 -2
  22. data/docs/directives-reference.md +593 -78
  23. data/docs/faq.md +85 -3
  24. data/docs/guides/available-models.md +1 -1
  25. data/docs/guides/basic-usage.md +6 -6
  26. data/docs/guides/chat.md +40 -16
  27. data/docs/guides/crew.md +239 -0
  28. data/docs/guides/executable-prompts.md +1 -1
  29. data/docs/guides/index.md +1 -0
  30. data/docs/guides/models.md +15 -0
  31. data/docs/index.md +29 -2
  32. data/docs/installation.md +44 -17
  33. data/docs/mcp-integration.md +40 -0
  34. data/docs/prompt_management.md +85 -86
  35. data/docs/security.md +47 -0
  36. data/docs/special_projects_guide.md +386 -0
  37. data/docs/tools-and-mcp-examples.md +23 -0
  38. data/docs/workflows-and-pipelines.md +84 -7
  39. data/examples/.gitignore +1 -0
  40. data/examples/00_setup_aia.sh +27 -44
  41. data/examples/11_multi_model.sh +4 -14
  42. data/examples/12_token_usage.sh +3 -12
  43. data/examples/18_tools.sh +10 -2
  44. data/examples/22_chat_mode.sh +0 -10
  45. data/examples/23_verify.sh +139 -0
  46. data/examples/24_decompose.sh +139 -0
  47. data/examples/25_spawn.sh +139 -0
  48. data/examples/26_debate.sh +97 -0
  49. data/examples/27_mention_routing.sh +157 -0
  50. data/examples/28_model_switching.sh +106 -0
  51. data/examples/29_agent_harness.sh +177 -0
  52. data/examples/README.md +65 -0
  53. data/examples/advanced_multi_robot_capabilities_without_examples.md +106 -0
  54. data/examples/aia_config.yml +1 -1
  55. data/examples/aia_config_orchestrator.yml +45 -0
  56. data/examples/common.sh +18 -6
  57. data/examples/context/tech_stack.md +2 -2
  58. data/examples/prompts_dir/roles/orchestrator.md +21 -0
  59. data/examples/requirements/sinatra_taskflow_app.md +139 -0
  60. data/examples/rules/01_classify_ruby.rb +16 -0
  61. data/examples/rules/02_prefer_claude_for_code.rb +19 -0
  62. data/examples/rules/03_gate_prompt_length.rb +19 -0
  63. data/examples/rules/04_tool_selection.rb +41 -0
  64. data/examples/rules/README.md +30 -0
  65. data/examples/run_all.sh +48 -15
  66. data/examples/tools/word_count_tool.rb +1 -1
  67. data/lib/AGENTS.md +57 -0
  68. data/lib/aia/chat_loop.rb +263 -167
  69. data/lib/aia/config/cli_parser.rb +217 -145
  70. data/lib/aia/config/defaults.yml +62 -33
  71. data/lib/aia/config/mcp_parser.rb +52 -51
  72. data/lib/aia/config/model_spec.rb +34 -2
  73. data/lib/aia/config/validator.rb +171 -216
  74. data/lib/aia/config.rb +111 -145
  75. data/lib/aia/content_extractor.rb +155 -0
  76. data/lib/aia/cost_calculator.rb +39 -0
  77. data/lib/aia/crew.rb +164 -0
  78. data/lib/aia/debate_handler.rb +174 -0
  79. data/lib/aia/delegate_handler.rb +116 -0
  80. data/lib/aia/directive.rb +43 -26
  81. data/lib/aia/directive_processor.rb +16 -7
  82. data/lib/aia/directives/configuration_directives.rb +214 -60
  83. data/lib/aia/directives/context_directives.rb +67 -52
  84. data/lib/aia/directives/execution_directives.rb +141 -4
  85. data/lib/aia/directives/model_directives.rb +163 -141
  86. data/lib/aia/directives/trakflow_directives.rb +62 -0
  87. data/lib/aia/directives/utility_directives.rb +227 -30
  88. data/lib/aia/directives/web_and_file_directives.rb +126 -77
  89. data/lib/aia/errors.rb +15 -0
  90. data/lib/aia/fact_asserter.rb +27 -0
  91. data/lib/aia/fzf.rb +9 -31
  92. data/lib/aia/handler_context.rb +17 -0
  93. data/lib/aia/handler_protocol.rb +19 -0
  94. data/lib/aia/history_transfer.rb +55 -0
  95. data/lib/aia/input_collector.rb +3 -3
  96. data/lib/aia/layered_orchestrator.rb +471 -0
  97. data/lib/aia/logger.rb +45 -25
  98. data/lib/aia/mcp_config_normalizer.rb +35 -0
  99. data/lib/aia/mcp_connection_manager.rb +315 -0
  100. data/lib/aia/mcp_discovery.rb +44 -0
  101. data/lib/aia/mcp_grouper.rb +33 -0
  102. data/lib/aia/mcp_server_config.rb +30 -0
  103. data/lib/aia/mcp_utility.rb +60 -0
  104. data/lib/aia/mention_router.rb +217 -0
  105. data/lib/aia/model_alias_registry.rb +97 -0
  106. data/lib/aia/model_switch_handler.rb +100 -0
  107. data/lib/aia/network_builder.rb +160 -0
  108. data/lib/aia/network_memory_manager.rb +55 -0
  109. data/lib/aia/patches/ruby_llm_streaming_error.rb +43 -0
  110. data/lib/aia/patches/ruby_llm_tool_error.rb +96 -0
  111. data/lib/aia/pipeline_orchestrator.rb +272 -0
  112. data/lib/aia/plugin_loader.rb +170 -0
  113. data/lib/aia/plugin_monitor.rb +211 -0
  114. data/lib/aia/prompt_decomposer.rb +159 -0
  115. data/lib/aia/prompt_handler.rb +56 -82
  116. data/lib/aia/robot_builder.rb +51 -0
  117. data/lib/aia/robot_factory.rb +338 -0
  118. data/lib/aia/robot_namer.rb +110 -0
  119. data/lib/aia/session.rb +87 -17
  120. data/lib/aia/session_tracker.rb +207 -0
  121. data/lib/aia/similarity_scorer.rb +41 -0
  122. data/lib/aia/skill_utils.rb +105 -1
  123. data/lib/aia/spawn_handler.rb +129 -0
  124. data/lib/aia/spawn_spec_parser.rb +65 -0
  125. data/lib/aia/special_mode_handler.rb +322 -0
  126. data/lib/aia/speech.rb +67 -0
  127. data/lib/aia/startup_coordinator.rb +151 -0
  128. data/lib/aia/streaming_runner.rb +172 -0
  129. data/lib/aia/system_prompt_assembler.rb +92 -0
  130. data/lib/aia/task_coordinator.rb +207 -0
  131. data/lib/aia/task_decomposer.rb +57 -0
  132. data/lib/aia/task_executor.rb +51 -0
  133. data/lib/aia/tfidf_math.rb +27 -0
  134. data/lib/aia/timing.rb +15 -0
  135. data/lib/aia/tool_filter/tfidf.rb +116 -0
  136. data/lib/aia/tool_filter/wordnet_expander.rb +127 -0
  137. data/lib/aia/tool_filter.rb +83 -0
  138. data/lib/aia/tool_filter_registry.rb +30 -0
  139. data/lib/aia/tool_filter_strategy.rb +146 -0
  140. data/lib/aia/tool_introspection.rb +17 -0
  141. data/lib/aia/tool_loader.rb +216 -0
  142. data/lib/aia/tool_utility.rb +30 -0
  143. data/lib/aia/tools/delegate_to_foreman_tool.rb +70 -0
  144. data/lib/aia/tools/recruit_robot_tool.rb +60 -0
  145. data/lib/aia/tools/reskill_robot_tool.rb +44 -0
  146. data/lib/aia/tools/task_board_tool.rb +115 -0
  147. data/lib/aia/trakflow_bridge.rb +175 -0
  148. data/lib/aia/turn_state.rb +95 -0
  149. data/lib/aia/ui_presenter.rb +182 -206
  150. data/lib/aia/utility.rb +136 -87
  151. data/lib/aia/{history_manager.rb → variable_input_collector.rb} +9 -9
  152. data/lib/aia/verification_network.rb +57 -0
  153. data/lib/aia.rb +124 -63
  154. data/mkdocs.yml +1 -0
  155. metadata +187 -58
  156. data/justfile +0 -215
  157. data/lib/aia/adapter/chat_execution.rb +0 -242
  158. data/lib/aia/adapter/error_handler.rb +0 -68
  159. data/lib/aia/adapter/gem_activator.rb +0 -57
  160. data/lib/aia/adapter/mcp_connector.rb +0 -274
  161. data/lib/aia/adapter/modality_handlers.rb +0 -167
  162. data/lib/aia/adapter/model_registry.rb +0 -81
  163. data/lib/aia/adapter/multi_model_chat.rb +0 -218
  164. data/lib/aia/adapter/provider_configurator.rb +0 -59
  165. data/lib/aia/adapter/tool_filter.rb +0 -85
  166. data/lib/aia/adapter/tool_loader.rb +0 -90
  167. data/lib/aia/chat_processor_service.rb +0 -178
  168. data/lib/aia/prompt_pipeline.rb +0 -183
  169. data/lib/aia/ruby_llm_adapter.rb +0 -95
  170. data/lib/extensions/openstruct_merge.rb +0 -48
  171. data/lib/extensions/ruby_llm/.irbrc +0 -56
  172. data/lib/extensions/ruby_llm/modalities.rb +0 -36
  173. data/lib/extensions/ruby_llm/provider_fix.rb +0 -79
  174. data/lib/refinements/string.rb +0 -16
  175. 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 AIA.config.to_h
11
- ""
14
+ ap cfg.to_h
12
15
  elsif args.length == 1
13
16
  config_item = args.first
14
17
  local_cfg = {}
15
- if AIA.config.respond_to?(config_item)
16
- local_cfg[config_item] = AIA.config.send(config_item)
17
- else
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
- boolean = AIA.respond_to?("#{config_item}?")
25
- new_value = args.join(' ').gsub('=', '').strip
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
- setter = "#{config_item}="
32
- if AIA.config.respond_to?(setter)
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
- warn "Warning: Unknown config option '#{config_item}'"
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
- alias_method :cfg, :config
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 = AIA.config.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
- AIA.config.models = AIA::Config::TO_MODEL_SPECS.call(model_names)
91
- AIA.client = AIA.client.class.new
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
- alias_method :temp, :temperature
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
- alias_method :topp, :top_p
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 { |chat|
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
- alias_method :ckp, :checkpoint
48
- alias_method :cp, :checkpoint
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
- chats = get_chats
71
+ robots = get_robots
68
72
 
69
- return "Error: No active chat sessions found." if chats.nil? || chats.empty?
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
- chat = chats[model_id]
73
- next unless chat
76
+ robot = robots[model_id]
77
+ next unless robot
74
78
 
75
79
  restored_messages = saved_messages.map { |msg| deep_copy_message(msg) }
76
- chat.instance_variable_set(:@messages, restored_messages)
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 > 0
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
- chats = get_chats
94
- return "Error: No active chat sessions found." if chats.nil? || chats.empty?
97
+ robots = get_robots
98
+ return "Error: No active chat sessions found." if robots.nil? || robots.empty?
95
99
 
96
- chats.each do |_model_id, chat|
97
- if keep_system
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
- first_chat = chats.values.first
119
- messages = first_chat&.messages || []
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
- if positions[index]
131
- puts "📍 [Checkpoint: #{positions[index].join(', ')}]"
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
- alias_method :context, :review
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 data[:topic_preview] && !data[:topic_preview].empty?
164
- puts " → \"#{data[:topic_preview]}\""
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
- alias_method :checkpoints, :checkpoints_list
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
- return nil unless AIA.client.respond_to?(:chats)
205
- AIA.client.chats
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.reverse.find { |msg| msg.role == :user }
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