aia 1.1.1 → 2.0.0.0.pre.alpha
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 +5 -1
- data/.loki +231 -0
- data/.quality/flay_baseline.txt +1 -0
- data/.quality/flog_baseline.txt +29 -0
- data/.quality/reek_baseline.txt +80 -0
- data/.rubocop.yml +116 -0
- data/.version +1 -1
- data/CHANGELOG.md +259 -50
- data/IMPLEMENTATION_PLAN.md +506 -0
- data/README.md +266 -238
- data/Rakefile +118 -5
- data/architecture_review.md +314 -0
- data/bin/aia +16 -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 +304 -164
- data/lib/aia/config/cli_parser.rb +174 -111
- data/lib/aia/config/defaults.yml +62 -33
- data/lib/aia/config/mcp_parser.rb +39 -46
- data/lib/aia/config/model_spec.rb +34 -2
- data/lib/aia/config/validator.rb +108 -142
- data/lib/aia/config.rb +110 -145
- data/lib/aia/content_extractor.rb +153 -0
- data/lib/aia/cost_calculator.rb +38 -0
- data/lib/aia/crew.rb +164 -0
- data/lib/aia/debate_handler.rb +166 -0
- data/lib/aia/delegate_handler.rb +112 -0
- data/lib/aia/directive.rb +33 -18
- data/lib/aia/directive_processor.rb +16 -7
- data/lib/aia/directives/configuration_directives.rb +160 -20
- data/lib/aia/directives/context_directives.rb +38 -26
- data/lib/aia/directives/execution_directives.rb +136 -4
- data/lib/aia/directives/model_directives.rb +76 -34
- data/lib/aia/directives/trakflow_directives.rb +44 -0
- data/lib/aia/directives/utility_directives.rb +203 -6
- data/lib/aia/directives/web_and_file_directives.rb +96 -60
- 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 +448 -0
- data/lib/aia/logger.rb +24 -4
- data/lib/aia/mcp_config_normalizer.rb +35 -0
- data/lib/aia/mcp_connection_manager.rb +305 -0
- data/lib/aia/mcp_discovery.rb +44 -0
- data/lib/aia/mcp_grouper.rb +33 -0
- data/lib/aia/mcp_utility.rb +57 -0
- data/lib/aia/mention_router.rb +260 -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 +155 -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 +262 -0
- data/lib/aia/plugin_loader.rb +170 -0
- data/lib/aia/plugin_monitor.rb +208 -0
- data/lib/aia/prompt_decomposer.rb +157 -0
- data/lib/aia/prompt_handler.rb +19 -39
- data/lib/aia/robot_builder.rb +51 -0
- data/lib/aia/robot_factory.rb +334 -0
- data/lib/aia/robot_namer.rb +116 -0
- data/lib/aia/session.rb +83 -17
- data/lib/aia/session_tracker.rb +209 -0
- data/lib/aia/similarity_scorer.rb +39 -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 +302 -0
- data/lib/aia/startup_coordinator.rb +150 -0
- data/lib/aia/streaming_runner.rb +169 -0
- data/lib/aia/system_prompt_assembler.rb +88 -0
- data/lib/aia/task_coordinator.rb +202 -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/tool_filter/tfidf.rb +116 -0
- data/lib/aia/tool_filter/wordnet_expander.rb +127 -0
- data/lib/aia/tool_filter.rb +82 -0
- data/lib/aia/tool_filter_registry.rb +30 -0
- data/lib/aia/tool_filter_strategy.rb +143 -0
- data/lib/aia/tool_loader.rb +210 -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 +114 -0
- data/lib/aia/trakflow_bridge.rb +173 -0
- data/lib/aia/turn_state.rb +94 -0
- data/lib/aia/ui_presenter.rb +166 -198
- data/lib/aia/utility.rb +134 -87
- data/lib/aia/{history_manager.rb → variable_input_collector.rb} +8 -9
- data/lib/aia/verification_network.rb +58 -0
- data/lib/aia.rb +108 -63
- data/mkdocs.yml +1 -0
- metadata +179 -56
- 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,45 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
module AIA
|
|
4
4
|
class ConfigurationDirectives < Directive
|
|
5
|
+
state_setting! :model
|
|
6
|
+
|
|
5
7
|
desc "View or set configuration values"
|
|
6
8
|
def config(args = [], context_manager = nil)
|
|
7
9
|
args = Array(args)
|
|
8
10
|
|
|
9
11
|
if args.empty?
|
|
10
12
|
ap AIA.config.to_h
|
|
11
|
-
""
|
|
12
13
|
elsif args.length == 1
|
|
13
14
|
config_item = args.first
|
|
14
15
|
local_cfg = {}
|
|
15
|
-
if AIA.config.respond_to?(config_item)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
local_cfg[config_item] = nil
|
|
19
|
-
end
|
|
16
|
+
local_cfg[config_item] = if AIA.config.respond_to?(config_item)
|
|
17
|
+
AIA.config.send(config_item)
|
|
18
|
+
end
|
|
20
19
|
ap local_cfg
|
|
21
|
-
""
|
|
22
20
|
else
|
|
23
21
|
config_item = args.shift
|
|
24
|
-
|
|
25
|
-
new_value = args.join(' ').gsub('=', '').strip
|
|
22
|
+
new_value = args.join(' ').gsub('=', '').strip
|
|
26
23
|
|
|
24
|
+
# Resolve the leaf key name regardless of dot-notation (flags.debug → debug)
|
|
25
|
+
leaf_key = config_item.to_s.split('.').last
|
|
26
|
+
|
|
27
|
+
boolean = AIA.respond_to?("#{leaf_key}?")
|
|
27
28
|
if boolean
|
|
28
29
|
new_value = %w[true t yes y on 1 yea yeah yep yup].include?(new_value.downcase)
|
|
29
30
|
end
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
AIA.config.send(setter, new_value)
|
|
32
|
+
if write_config_value(config_item, new_value)
|
|
33
|
+
AIA::LoggerManager.reconfigure_levels!
|
|
34
34
|
else
|
|
35
|
-
|
|
35
|
+
$stderr.puts "Warning: Unknown config option '#{config_item}'"
|
|
36
36
|
AIA::LoggerManager.aia_logger.warn("Unknown config option '#{config_item}'")
|
|
37
37
|
end
|
|
38
|
-
""
|
|
39
38
|
end
|
|
39
|
+
""
|
|
40
40
|
end
|
|
41
|
-
|
|
41
|
+
alias cfg config
|
|
42
42
|
|
|
43
43
|
desc "View or change the AI model"
|
|
44
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
44
45
|
def model(args, context_manager = nil)
|
|
45
46
|
if args.empty?
|
|
46
47
|
puts
|
|
@@ -49,7 +50,13 @@ module AIA
|
|
|
49
50
|
if models.size == 1
|
|
50
51
|
puts "Current Model:"
|
|
51
52
|
puts "=============="
|
|
52
|
-
|
|
53
|
+
model_name = models.first.respond_to?(:name) ? models.first.name : models.first.to_s
|
|
54
|
+
begin
|
|
55
|
+
model_info = RubyLLM::Models.find(model_name)
|
|
56
|
+
puts model_info.to_h.pretty_inspect if model_info
|
|
57
|
+
rescue StandardError
|
|
58
|
+
puts " #{model_name}"
|
|
59
|
+
end
|
|
53
60
|
else
|
|
54
61
|
puts "Multi-Model Configuration:"
|
|
55
62
|
puts "=========================="
|
|
@@ -64,7 +71,7 @@ module AIA
|
|
|
64
71
|
|
|
65
72
|
models.each_with_index do |model_spec, index|
|
|
66
73
|
model_name = model_spec.respond_to?(:name) ? model_spec.name : model_spec.to_s
|
|
67
|
-
puts "#{index + 1}. #{model_name}#{
|
|
74
|
+
puts "#{index + 1}. #{model_name}#{' (primary)' if index.zero?}"
|
|
68
75
|
|
|
69
76
|
begin
|
|
70
77
|
model_info = RubyLLM::Models.find(model_name)
|
|
@@ -88,22 +95,155 @@ module AIA
|
|
|
88
95
|
else
|
|
89
96
|
model_names = args.join(' ').split(',').map(&:strip).reject(&:empty?)
|
|
90
97
|
AIA.config.models = AIA::Config::TO_MODEL_SPECS.call(model_names)
|
|
91
|
-
AIA.client = AIA.
|
|
98
|
+
AIA.client = RobotFactory.rebuild(AIA.config)
|
|
92
99
|
end
|
|
93
100
|
|
|
94
101
|
''
|
|
95
102
|
end
|
|
103
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
96
104
|
|
|
97
105
|
desc "Set the temperature parameter for AI responses"
|
|
98
106
|
def temperature(args, context_manager = nil)
|
|
99
107
|
config(args.prepend('temperature'), context_manager)
|
|
100
108
|
end
|
|
101
|
-
|
|
109
|
+
alias temp temperature
|
|
102
110
|
|
|
103
111
|
desc "Set the top_p parameter for AI responses"
|
|
104
112
|
def top_p(args, context_manager = nil)
|
|
105
113
|
config(args.prepend('top_p'), context_manager)
|
|
106
114
|
end
|
|
107
|
-
|
|
115
|
+
alias topp top_p
|
|
116
|
+
|
|
117
|
+
desc "Dump session cost/token metrics as CSV"
|
|
118
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
119
|
+
def cost(args = [], context_manager = nil)
|
|
120
|
+
tracker = AIA.session_tracker
|
|
121
|
+
unless tracker
|
|
122
|
+
puts "No session tracker available."
|
|
123
|
+
return ''
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
turns = tracker.turns.reject { |t| t[:type] == :model_switch }
|
|
127
|
+
if turns.empty?
|
|
128
|
+
puts "No turns recorded yet."
|
|
129
|
+
return ''
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
has_similarity = turns.any? { |t| t.key?(:similarity) }
|
|
133
|
+
|
|
134
|
+
header = %w[model input_tokens output_tokens total_tokens cost elapsed]
|
|
135
|
+
header << 'similarity' if has_similarity
|
|
136
|
+
lines = [header.join(',')]
|
|
137
|
+
|
|
138
|
+
turns.each do |turn|
|
|
139
|
+
input_t = turn[:input_tokens] || 0
|
|
140
|
+
output_t = turn[:output_tokens] || 0
|
|
141
|
+
total_t = input_t + output_t
|
|
142
|
+
cost_val = turn[:cost] || 0.0
|
|
143
|
+
elapsed = turn[:elapsed] || 0
|
|
144
|
+
|
|
145
|
+
row = [
|
|
146
|
+
turn[:model],
|
|
147
|
+
input_t,
|
|
148
|
+
output_t,
|
|
149
|
+
total_t,
|
|
150
|
+
"$#{'%.5f' % cost_val}",
|
|
151
|
+
"%.1fs" % elapsed
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
if has_similarity
|
|
155
|
+
sim = turn[:similarity]
|
|
156
|
+
row << (sim.nil? ? 'ref' : "%.1f%%" % (sim * 100))
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
lines << row.join(',')
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Totals
|
|
163
|
+
total_input = turns.sum { |t| t[:input_tokens] || 0 }
|
|
164
|
+
total_output = turns.sum { |t| t[:output_tokens] || 0 }
|
|
165
|
+
total_tokens = total_input + total_output
|
|
166
|
+
total_cost = turns.sum { |t| t[:cost] || 0.0 }
|
|
167
|
+
max_elapsed = turns.map { |t| t[:elapsed] || 0 }.max
|
|
168
|
+
|
|
169
|
+
totals = [
|
|
170
|
+
'TOTAL',
|
|
171
|
+
total_input,
|
|
172
|
+
total_output,
|
|
173
|
+
total_tokens,
|
|
174
|
+
"$#{'%.5f' % total_cost}",
|
|
175
|
+
"%.1fs" % max_elapsed
|
|
176
|
+
]
|
|
177
|
+
totals << '' if has_similarity
|
|
178
|
+
lines << totals.join(',')
|
|
179
|
+
|
|
180
|
+
csv = lines.join("\n")
|
|
181
|
+
puts
|
|
182
|
+
puts csv
|
|
183
|
+
puts
|
|
184
|
+
|
|
185
|
+
out_file = AIA.config.output&.file
|
|
186
|
+
File.open(out_file, 'a') { |f| f.puts csv } if out_file
|
|
187
|
+
|
|
188
|
+
''
|
|
189
|
+
end
|
|
190
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
191
|
+
|
|
192
|
+
private
|
|
193
|
+
|
|
194
|
+
# Apply a config value by key, supporting three forms:
|
|
195
|
+
# flat CLI key — "debug" → CLI_TO_NESTED_MAP routing
|
|
196
|
+
# dot-notation — "flags.debug" → navigate section.subkey
|
|
197
|
+
# direct setter — "log_level_override" → AIA.config.<key>=
|
|
198
|
+
#
|
|
199
|
+
# Returns true if the value was set, false if the key was unknown.
|
|
200
|
+
def write_config_value(config_item, new_value) # rubocop:disable Naming/PredicateMethod
|
|
201
|
+
key_str = config_item.to_s
|
|
202
|
+
|
|
203
|
+
# Dot-notation: walk the config object by path segments
|
|
204
|
+
if key_str.include?('.')
|
|
205
|
+
parts = key_str.split('.')
|
|
206
|
+
obj = AIA.config
|
|
207
|
+
parents = parts[0..-2]
|
|
208
|
+
leaf = parts.last
|
|
209
|
+
|
|
210
|
+
parents.each do |part|
|
|
211
|
+
obj = obj.respond_to?(part) ? obj.send(part) : nil
|
|
212
|
+
return false if obj.nil?
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
if obj.respond_to?("#{leaf}=")
|
|
216
|
+
obj.send("#{leaf}=", new_value)
|
|
217
|
+
maybe_clear_log_override(leaf, new_value)
|
|
218
|
+
return true
|
|
219
|
+
end
|
|
220
|
+
return false
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Flat CLI key via CLI_TO_NESTED_MAP (e.g. "debug" → flags.debug)
|
|
224
|
+
if AIA::Config::CLI_TO_NESTED_MAP.key?(key_str.to_sym) &&
|
|
225
|
+
AIA.config.respond_to?(:apply_overrides)
|
|
226
|
+
AIA.config.apply_overrides({ key_str.to_sym => new_value })
|
|
227
|
+
maybe_clear_log_override(key_str, new_value)
|
|
228
|
+
return true
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Direct setter on the config object (e.g. "log_level_override")
|
|
232
|
+
if AIA.config.respond_to?("#{key_str}=")
|
|
233
|
+
AIA.config.send("#{key_str}=", new_value)
|
|
234
|
+
return true
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
false
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# When debug is disabled, clear log_level_override so the logger
|
|
241
|
+
# drops back to its configured level rather than staying at DEBUG.
|
|
242
|
+
def maybe_clear_log_override(key, value)
|
|
243
|
+
return unless key.to_s == 'debug' && value == false
|
|
244
|
+
return unless AIA.config.respond_to?(:log_level_override=)
|
|
245
|
+
|
|
246
|
+
AIA.config.log_level_override = nil if AIA.config.log_level_override.to_s == 'debug'
|
|
247
|
+
end
|
|
108
248
|
end
|
|
109
249
|
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
|
|
@@ -32,9 +34,9 @@ module AIA
|
|
|
32
34
|
|
|
33
35
|
first_chat_messages = chats.values.first&.messages || []
|
|
34
36
|
@checkpoint_store[name] = {
|
|
35
|
-
messages: chats.transform_values
|
|
37
|
+
messages: chats.transform_values do |chat|
|
|
36
38
|
chat.messages.map { |msg| deep_copy_message(msg) }
|
|
37
|
-
|
|
39
|
+
end,
|
|
38
40
|
position: first_chat_messages.size,
|
|
39
41
|
created_at: Time.now,
|
|
40
42
|
topic_preview: extract_last_user_message(first_chat_messages)
|
|
@@ -44,8 +46,8 @@ module AIA
|
|
|
44
46
|
puts "Checkpoint '#{name}' created at position #{@checkpoint_store[name][:position]}."
|
|
45
47
|
""
|
|
46
48
|
end
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
alias ckp checkpoint
|
|
50
|
+
alias cp checkpoint
|
|
49
51
|
|
|
50
52
|
desc "Restore context to a previous checkpoint"
|
|
51
53
|
def restore(args, _unused = nil)
|
|
@@ -64,16 +66,16 @@ module AIA
|
|
|
64
66
|
end
|
|
65
67
|
|
|
66
68
|
checkpoint_data = @checkpoint_store[name]
|
|
67
|
-
|
|
69
|
+
robots = get_robots
|
|
68
70
|
|
|
69
|
-
return "Error: No active chat sessions found." if
|
|
71
|
+
return "Error: No active chat sessions found." if robots.nil? || robots.empty?
|
|
70
72
|
|
|
71
73
|
checkpoint_data[:messages].each do |model_id, saved_messages|
|
|
72
|
-
|
|
73
|
-
next unless
|
|
74
|
+
robot = robots[model_id]
|
|
75
|
+
next unless robot
|
|
74
76
|
|
|
75
77
|
restored_messages = saved_messages.map { |msg| deep_copy_message(msg) }
|
|
76
|
-
|
|
78
|
+
robot.replace_messages(restored_messages)
|
|
77
79
|
end
|
|
78
80
|
|
|
79
81
|
restored_position = checkpoint_data[:position]
|
|
@@ -82,7 +84,7 @@ module AIA
|
|
|
82
84
|
@last_checkpoint_name = name
|
|
83
85
|
|
|
84
86
|
msg = "Context restored to checkpoint '#{name}' (position #{restored_position})."
|
|
85
|
-
msg += " Removed #{removed_count} checkpoint(s) that were beyond this position." if removed_count
|
|
87
|
+
msg += " Removed #{removed_count} checkpoint(s) that were beyond this position." if removed_count.positive?
|
|
86
88
|
msg
|
|
87
89
|
end
|
|
88
90
|
|
|
@@ -90,17 +92,11 @@ module AIA
|
|
|
90
92
|
def clear(args, _unused = nil)
|
|
91
93
|
keep_system = !args.include?('--all')
|
|
92
94
|
|
|
93
|
-
|
|
94
|
-
return "Error: No active chat sessions found." if
|
|
95
|
+
robots = get_robots
|
|
96
|
+
return "Error: No active chat sessions found." if robots.nil? || robots.empty?
|
|
95
97
|
|
|
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
|
|
98
|
+
robots.each_value do |robot|
|
|
99
|
+
robot.clear_messages(keep_system: keep_system)
|
|
104
100
|
end
|
|
105
101
|
|
|
106
102
|
@checkpoint_store.clear
|
|
@@ -111,6 +107,7 @@ module AIA
|
|
|
111
107
|
end
|
|
112
108
|
|
|
113
109
|
desc "Display the current conversation context with checkpoint markers"
|
|
110
|
+
# rubocop:disable Metrics/AbcSize
|
|
114
111
|
def review(args, _unused = nil)
|
|
115
112
|
chats = get_chats
|
|
116
113
|
return "Error: No active chat sessions found." if chats.nil? || chats.empty?
|
|
@@ -147,7 +144,8 @@ module AIA
|
|
|
147
144
|
puts "=== End of Context ==="
|
|
148
145
|
""
|
|
149
146
|
end
|
|
150
|
-
|
|
147
|
+
# rubocop:enable Metrics/AbcSize
|
|
148
|
+
alias context review
|
|
151
149
|
|
|
152
150
|
desc "List all available checkpoints"
|
|
153
151
|
def checkpoints_list(args, _unused = nil)
|
|
@@ -167,7 +165,7 @@ module AIA
|
|
|
167
165
|
puts "=== End of Checkpoints ==="
|
|
168
166
|
""
|
|
169
167
|
end
|
|
170
|
-
|
|
168
|
+
alias checkpoints checkpoints_list
|
|
171
169
|
|
|
172
170
|
# --- helpers (no desc → not registered as directives) ---
|
|
173
171
|
|
|
@@ -200,9 +198,23 @@ module AIA
|
|
|
200
198
|
|
|
201
199
|
private
|
|
202
200
|
|
|
201
|
+
# In v2, AIA.client is a RobotLab::Robot which wraps a RubyLLM::Chat.
|
|
202
|
+
# Returns a hash of { name => Robot } for checkpoint operations.
|
|
203
|
+
def get_robots
|
|
204
|
+
robot = AIA.client
|
|
205
|
+
return nil unless robot
|
|
206
|
+
|
|
207
|
+
name = robot.respond_to?(:name) ? robot.name : "default"
|
|
208
|
+
{ name => robot }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Return { name => RubyLLM::Chat } for operations that need direct chat access.
|
|
212
|
+
# Uses Robot#chat public API.
|
|
203
213
|
def get_chats
|
|
204
|
-
|
|
205
|
-
|
|
214
|
+
robots = get_robots
|
|
215
|
+
return nil unless robots
|
|
216
|
+
|
|
217
|
+
robots.transform_values(&:chat)
|
|
206
218
|
end
|
|
207
219
|
|
|
208
220
|
def deep_copy_message(msg)
|
|
@@ -233,12 +245,12 @@ module AIA
|
|
|
233
245
|
def extract_last_user_message(messages, max_length: 70)
|
|
234
246
|
return "" if messages.nil? || messages.empty?
|
|
235
247
|
|
|
236
|
-
last_user_msg = messages.
|
|
248
|
+
last_user_msg = messages.rfind { |msg| msg.role == :user }
|
|
237
249
|
return "" unless last_user_msg
|
|
238
250
|
|
|
239
251
|
content = last_user_msg.content.to_s.strip
|
|
240
252
|
content = content.gsub(/\s+/, ' ')
|
|
241
|
-
content.length > max_length ? "#{content[0..max_length - 4]}..." : content
|
|
253
|
+
content.length > max_length ? "#{content[0..(max_length - 4)]}..." : content
|
|
242
254
|
end
|
|
243
255
|
end
|
|
244
256
|
end
|
|
@@ -1,15 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# lib/aia/directives/execution_directives.rb
|
|
2
4
|
|
|
3
5
|
require 'shellwords'
|
|
4
6
|
|
|
5
7
|
module AIA
|
|
6
8
|
class ExecutionDirectives < Directive
|
|
7
|
-
|
|
9
|
+
state_setting! :concurrent, :conc, :verify, :decompose, :debate,
|
|
10
|
+
:delegate, :del, :spawn, :orchestrate, :orch,
|
|
11
|
+
:add_recruit, :add, :drop_recruit, :drop, :reskill
|
|
12
|
+
|
|
13
|
+
desc "Execute Ruby code (requires allow_ruby_eval: true in config)"
|
|
8
14
|
def ruby(args, context_manager = nil)
|
|
15
|
+
unless AIA.config.flags.allow_ruby_eval
|
|
16
|
+
return "ERROR: /ruby is disabled. Set `allow_ruby_eval: true` in " \
|
|
17
|
+
"~/.aia/config.yml or pass --allow-ruby-eval to enable it. " \
|
|
18
|
+
"Note: this executes arbitrary Ruby with full process privileges."
|
|
19
|
+
end
|
|
20
|
+
|
|
9
21
|
ruby_code = args.join(' ')
|
|
22
|
+
$stderr.puts "WARNING: /ruby executing: #{ruby_code}"
|
|
10
23
|
|
|
11
24
|
begin
|
|
12
|
-
String(eval(ruby_code))
|
|
25
|
+
String(eval(ruby_code)) # rubocop:disable Security/Eval
|
|
13
26
|
rescue StandardError => e
|
|
14
27
|
<<~ERROR
|
|
15
28
|
This ruby code failed: #{ruby_code}
|
|
@@ -17,12 +30,131 @@ module AIA
|
|
|
17
30
|
ERROR
|
|
18
31
|
end
|
|
19
32
|
end
|
|
20
|
-
|
|
33
|
+
alias rb ruby
|
|
21
34
|
|
|
22
35
|
desc "Use text-to-speech to speak the text"
|
|
23
36
|
def say(args, context_manager = nil)
|
|
24
|
-
|
|
37
|
+
audio = AIA.config.audio
|
|
38
|
+
env = {}
|
|
39
|
+
env['SPEECH_MODEL'] = audio.speech_model if audio.speech_model
|
|
40
|
+
if audio.voice && !audio.voice.strip.empty?
|
|
41
|
+
system(env, 'say', '-v', audio.voice, *args)
|
|
42
|
+
else
|
|
43
|
+
system(env, 'say', *args)
|
|
44
|
+
end
|
|
25
45
|
""
|
|
26
46
|
end
|
|
47
|
+
|
|
48
|
+
desc "Execute next prompt with concurrent MCP server access"
|
|
49
|
+
def concurrent(args, context_manager = nil)
|
|
50
|
+
AIA.turn_state.force_concurrent_mcp = true
|
|
51
|
+
"Concurrent MCP mode enabled for next prompt."
|
|
52
|
+
end
|
|
53
|
+
alias conc concurrent
|
|
54
|
+
|
|
55
|
+
desc "Run next prompt through verification (two independent answers + reconciliation)"
|
|
56
|
+
def verify(args, context_manager = nil)
|
|
57
|
+
AIA.turn_state.force_verify = true
|
|
58
|
+
"Verification mode enabled for next prompt."
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
desc "Decompose next prompt into parallel sub-tasks"
|
|
62
|
+
def decompose(args, context_manager = nil)
|
|
63
|
+
AIA.turn_state.force_decompose = true
|
|
64
|
+
"Decomposition mode enabled for next prompt."
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
desc "Multi-round debate between robots in the network"
|
|
68
|
+
def debate(args, context_manager = nil)
|
|
69
|
+
AIA.turn_state.force_debate = true
|
|
70
|
+
"Debate mode enabled for next prompt."
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
desc "Delegate subtasks to specific robots via TrakFlow plan"
|
|
74
|
+
def delegate(args, context_manager = nil)
|
|
75
|
+
AIA.turn_state.force_delegate = true
|
|
76
|
+
"Delegation mode enabled for next prompt."
|
|
77
|
+
end
|
|
78
|
+
alias del delegate
|
|
79
|
+
|
|
80
|
+
desc "Spawn a specialist robot for the next prompt. " \
|
|
81
|
+
"Usage: /spawn | /spawn <type> | /spawn <name> <provider/model> <system prompt>"
|
|
82
|
+
def spawn(args, context_manager = nil)
|
|
83
|
+
AIA.turn_state.force_spawn = true
|
|
84
|
+
|
|
85
|
+
if args.size >= 2
|
|
86
|
+
spec = AIA::SpawnSpecParser.parse(args)
|
|
87
|
+
AIA.turn_state.spawn_spec = spec
|
|
88
|
+
AIA.turn_state.spawn_type = nil
|
|
89
|
+
"Spawn mode enabled: '#{spec[:name]}' (#{spawn_model_summary(spec)}) for next prompt."
|
|
90
|
+
else
|
|
91
|
+
AIA.turn_state.spawn_spec = nil
|
|
92
|
+
AIA.turn_state.spawn_type = args.first
|
|
93
|
+
type_msg = args.first ? " (#{args.first})" : " (auto-detect)"
|
|
94
|
+
"Spawn mode enabled#{type_msg} for next prompt."
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
desc "Recruit a robot into the crew (persists, @mention-able). Usage: " \
|
|
99
|
+
"/add_recruit <name> [provider/model] [skill:<id>...] [system prompt] (alias: /add)"
|
|
100
|
+
def add_recruit(args, context_manager = nil)
|
|
101
|
+
return "Usage: /add_recruit <name> [provider/model] [skill:<id> ...] [system prompt]" if args.empty?
|
|
102
|
+
|
|
103
|
+
spec = args.size >= 2 ? AIA::SpawnSpecParser.parse(args) : { name: args[0] }
|
|
104
|
+
robot = AIA::Crew.recruit(spec)
|
|
105
|
+
"Recruited '#{robot.name}' into the crew (#{recruit_summary(spec)})."
|
|
106
|
+
rescue AIA::CrewError => e
|
|
107
|
+
"Recruit failed: #{e.message}"
|
|
108
|
+
end
|
|
109
|
+
alias add add_recruit
|
|
110
|
+
|
|
111
|
+
desc "Drop a robot from the crew. Usage: /drop_recruit <name> (alias: /drop)"
|
|
112
|
+
def drop_recruit(args, context_manager = nil)
|
|
113
|
+
return "Usage: /drop_recruit <name>" if args.empty?
|
|
114
|
+
|
|
115
|
+
AIA::Crew.drop(args.first)
|
|
116
|
+
"Dropped '#{args.first}' from the crew."
|
|
117
|
+
rescue AIA::CrewError => e
|
|
118
|
+
"Drop failed: #{e.message}"
|
|
119
|
+
end
|
|
120
|
+
alias drop drop_recruit
|
|
121
|
+
|
|
122
|
+
desc "Reset a crew member to a clean slate and re-skill it. " \
|
|
123
|
+
"Usage: /reskill <name> [skill:<id>...] [system prompt]"
|
|
124
|
+
def reskill(args, context_manager = nil)
|
|
125
|
+
return "Usage: /reskill <name> [skill:<id> ...] [system prompt]" if args.empty?
|
|
126
|
+
|
|
127
|
+
skills, rest = AIA::SpawnSpecParser.extract_skills(args[1..].to_a)
|
|
128
|
+
prompt = rest.join(' ').strip
|
|
129
|
+
robot = AIA::Crew.reskill(args.first, skills: skills, system_prompt: (prompt.empty? ? nil : prompt))
|
|
130
|
+
suffix = skills.empty? ? '' : " with #{skills.join(', ')}"
|
|
131
|
+
"Reskilled '#{robot.name}'#{suffix} (clean slate)."
|
|
132
|
+
rescue AIA::CrewError => e
|
|
133
|
+
"Reskill failed: #{e.message}"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
desc "3-tier layered orchestration: orchestrator → lead agents → specialists"
|
|
137
|
+
def orchestrate(args, context_manager = nil)
|
|
138
|
+
AIA.turn_state.force_orchestrate = true
|
|
139
|
+
"Orchestration mode enabled. Your next prompt is the application requirements."
|
|
140
|
+
end
|
|
141
|
+
alias orch orchestrate
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
# Human-readable model description for the /spawn confirmation message.
|
|
146
|
+
def spawn_model_summary(spec)
|
|
147
|
+
return 'inherited model' unless spec[:model]
|
|
148
|
+
|
|
149
|
+
spec[:provider] ? "#{spec[:provider]}/#{spec[:model]}" : spec[:model]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Model plus any assigned skills, for the /add_recruit confirmation message.
|
|
153
|
+
def recruit_summary(spec)
|
|
154
|
+
parts = [spawn_model_summary(spec)]
|
|
155
|
+
skills = Array(spec[:skills])
|
|
156
|
+
parts << "skills: #{skills.join(', ')}" unless skills.empty?
|
|
157
|
+
parts.join('; ')
|
|
158
|
+
end
|
|
27
159
|
end
|
|
28
160
|
end
|