aia 2.0.0.0.pre.alpha → 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 +5 -1
- data/.loki +3 -223
- data/.reek.yml +160 -0
- data/.rubocop_strict.yml +15 -0
- data/.version +1 -1
- data/CHANGELOG.md +10 -5
- data/README.md +1 -1
- data/Rakefile +0 -113
- data/_typos.toml +10 -0
- data/aia.gemspec +92 -0
- data/config/aia.yml +13 -0
- data/lib/aia/chat_loop.rb +8 -52
- data/lib/aia/config/cli_parser.rb +66 -57
- data/lib/aia/config/mcp_parser.rb +27 -19
- data/lib/aia/config/validator.rb +83 -94
- data/lib/aia/config.rb +3 -2
- data/lib/aia/content_extractor.rb +2 -0
- data/lib/aia/cost_calculator.rb +1 -0
- data/lib/aia/debate_handler.rb +28 -20
- data/lib/aia/delegate_handler.rb +7 -3
- data/lib/aia/directive.rb +10 -8
- data/lib/aia/directives/configuration_directives.rb +115 -101
- data/lib/aia/directives/context_directives.rb +31 -28
- data/lib/aia/directives/execution_directives.rb +18 -13
- data/lib/aia/directives/model_directives.rb +134 -154
- data/lib/aia/directives/trakflow_directives.rb +27 -9
- data/lib/aia/directives/utility_directives.rb +71 -71
- data/lib/aia/directives/web_and_file_directives.rb +63 -50
- data/lib/aia/layered_orchestrator.rb +66 -43
- data/lib/aia/logger.rb +21 -21
- data/lib/aia/mcp_connection_manager.rb +50 -40
- data/lib/aia/mcp_server_config.rb +30 -0
- data/lib/aia/mcp_utility.rb +7 -4
- data/lib/aia/mention_router.rb +7 -50
- data/lib/aia/network_builder.rb +10 -5
- data/lib/aia/network_memory_manager.rb +6 -6
- data/lib/aia/pipeline_orchestrator.rb +20 -10
- data/lib/aia/plugin_monitor.rb +9 -6
- data/lib/aia/prompt_decomposer.rb +5 -3
- data/lib/aia/prompt_handler.rb +43 -49
- data/lib/aia/robot_factory.rb +12 -8
- data/lib/aia/robot_namer.rb +2 -8
- data/lib/aia/session.rb +7 -3
- data/lib/aia/session_tracker.rb +39 -41
- data/lib/aia/similarity_scorer.rb +5 -3
- data/lib/aia/special_mode_handler.rb +52 -32
- data/lib/aia/speech.rb +67 -0
- data/lib/aia/startup_coordinator.rb +1 -0
- data/lib/aia/streaming_runner.rb +6 -3
- data/lib/aia/system_prompt_assembler.rb +11 -7
- data/lib/aia/task_coordinator.rb +10 -5
- data/lib/aia/timing.rb +15 -0
- data/lib/aia/tool_filter.rb +1 -0
- data/lib/aia/tool_filter_strategy.rb +28 -25
- data/lib/aia/tool_introspection.rb +17 -0
- data/lib/aia/tool_loader.rb +12 -6
- data/lib/aia/tools/task_board_tool.rb +1 -0
- data/lib/aia/trakflow_bridge.rb +5 -3
- data/lib/aia/turn_state.rb +1 -0
- data/lib/aia/ui_presenter.rb +67 -59
- data/lib/aia/utility.rb +5 -3
- data/lib/aia/variable_input_collector.rb +1 -0
- data/lib/aia/verification_network.rb +1 -2
- data/lib/aia.rb +16 -0
- metadata +15 -9
- data/.quality/flay_baseline.txt +0 -1
- data/.quality/flog_baseline.txt +0 -29
- data/.quality/reek_baseline.txt +0 -80
data/lib/aia/config/validator.rb
CHANGED
|
@@ -16,6 +16,7 @@ module AIA
|
|
|
16
16
|
#
|
|
17
17
|
# @param config [AIA::Config] the configuration to validate
|
|
18
18
|
# @return [AIA::Config] the validated configuration
|
|
19
|
+
# :reek:TooManyStatements -- top-level validation pipeline: one named step per line, executed in fixed order
|
|
19
20
|
def tailor(config)
|
|
20
21
|
remaining_args = config.remaining_args&.dup || []
|
|
21
22
|
config.remaining_args = nil
|
|
@@ -101,24 +102,25 @@ module AIA
|
|
|
101
102
|
|
|
102
103
|
def handle_executable_prompt(config)
|
|
103
104
|
return unless config.prompt_id.nil?
|
|
104
|
-
return unless config.context_files && !config.context_files.empty?
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
files = config.context_files
|
|
107
|
+
return unless files && !files.empty?
|
|
108
|
+
|
|
109
|
+
candidate = files.first
|
|
107
110
|
return unless File.exist?(candidate) && File.readable?(candidate)
|
|
108
111
|
|
|
109
112
|
first_line = File.open(candidate, &:readline).strip rescue nil
|
|
110
113
|
return unless first_line&.start_with?('#!')
|
|
111
114
|
|
|
112
|
-
|
|
115
|
+
files.shift
|
|
113
116
|
config.executable_prompt_content = File.read(candidate).lines[1..].join
|
|
114
117
|
config.prompt_id = '__EXECUTABLE_PROMPT__'
|
|
115
118
|
end
|
|
116
119
|
|
|
117
120
|
def handle_stdin_as_prompt(config)
|
|
118
121
|
return unless config.prompt_id.nil?
|
|
119
|
-
return unless config.stdin_content && !config.stdin_content.strip.empty?
|
|
120
|
-
|
|
121
122
|
content = config.stdin_content
|
|
123
|
+
return unless content && !content.strip.empty?
|
|
122
124
|
|
|
123
125
|
if content.lines.first&.strip&.start_with?('#!')
|
|
124
126
|
content = content.lines[1..].join
|
|
@@ -137,24 +139,24 @@ module AIA
|
|
|
137
139
|
end
|
|
138
140
|
|
|
139
141
|
def process_role_configuration(config)
|
|
140
|
-
|
|
142
|
+
prompts = config.prompts
|
|
143
|
+
role = prompts.role
|
|
141
144
|
return if role.nil? || role.empty?
|
|
142
145
|
|
|
143
|
-
roles_prefix =
|
|
146
|
+
roles_prefix = prompts.roles_prefix
|
|
144
147
|
unless AIA::SkillUtils.path_based_id?(role) || roles_prefix.nil? || roles_prefix.empty? || role.start_with?(roles_prefix)
|
|
145
|
-
|
|
146
|
-
role =
|
|
148
|
+
role = "#{roles_prefix}/#{role}"
|
|
149
|
+
prompts.role = role
|
|
147
150
|
end
|
|
148
151
|
|
|
149
|
-
|
|
152
|
+
prompts.roles_dir ||= File.join(prompts.dir, roles_prefix.to_s)
|
|
150
153
|
|
|
151
154
|
return if config.flags&.chat == true
|
|
152
155
|
|
|
153
156
|
return unless config.prompt_id.nil? || config.prompt_id.empty?
|
|
154
|
-
return if role.nil? || role.empty?
|
|
155
157
|
config.prompt_id = role
|
|
156
|
-
config.pipeline.prepend(
|
|
157
|
-
|
|
158
|
+
config.pipeline.prepend(role)
|
|
159
|
+
prompts.role = ''
|
|
158
160
|
end
|
|
159
161
|
|
|
160
162
|
def validate_plugins_dir(config)
|
|
@@ -172,9 +174,10 @@ module AIA
|
|
|
172
174
|
end
|
|
173
175
|
|
|
174
176
|
def normalize_boolean_flags(config)
|
|
175
|
-
|
|
176
|
-
normalize_boolean_flag(
|
|
177
|
-
normalize_boolean_flag(
|
|
177
|
+
flags = config.flags
|
|
178
|
+
normalize_boolean_flag(flags, :chat)
|
|
179
|
+
normalize_boolean_flag(flags, :fuzzy)
|
|
180
|
+
normalize_boolean_flag(flags, :consensus)
|
|
178
181
|
end
|
|
179
182
|
|
|
180
183
|
def normalize_boolean_flag(flags_section, flag)
|
|
@@ -211,21 +214,19 @@ module AIA
|
|
|
211
214
|
else
|
|
212
215
|
label = mcp_filter_active?(config) ? "Active" : "Configured"
|
|
213
216
|
puts "#{label} MCP servers:\n\n"
|
|
214
|
-
servers.each
|
|
215
|
-
name = AIA::Utility.server_name(server) || '(unnamed)'
|
|
216
|
-
transport = server[:transport] || server['transport'] || {}
|
|
217
|
-
command = transport[:command] || transport['command'] || server[:command] || server['command'] || '(no command)'
|
|
218
|
-
args = transport[:args] || transport['args'] || server[:args] || server['args'] || []
|
|
219
|
-
args_str = args.empty? ? '' : " #{args.join(' ')}"
|
|
220
|
-
puts " #{name}"
|
|
221
|
-
puts " command: #{command}#{args_str}"
|
|
222
|
-
puts
|
|
223
|
-
end
|
|
217
|
+
servers.each { |server| print_mcp_server(McpServerConfig.from_hash(server)) }
|
|
224
218
|
end
|
|
225
219
|
|
|
226
220
|
:early_exit
|
|
227
221
|
end
|
|
228
222
|
|
|
223
|
+
def print_mcp_server(server_config)
|
|
224
|
+
args_str = server_config.args.empty? ? '' : " #{server_config.args.join(' ')}"
|
|
225
|
+
puts " #{server_config.name || '(unnamed)'}"
|
|
226
|
+
puts " command: #{server_config.command || '(no command)'}#{args_str}"
|
|
227
|
+
puts
|
|
228
|
+
end
|
|
229
|
+
|
|
229
230
|
def handle_list_tools(config)
|
|
230
231
|
return unless config.list_tools
|
|
231
232
|
|
|
@@ -266,8 +267,8 @@ module AIA
|
|
|
266
267
|
end
|
|
267
268
|
|
|
268
269
|
def print_tool_terminal(tool, width, indent)
|
|
269
|
-
name =
|
|
270
|
-
desc =
|
|
270
|
+
name = ToolIntrospection.tool_name(tool)
|
|
271
|
+
desc = ToolIntrospection.tool_description(tool).strip
|
|
271
272
|
|
|
272
273
|
puts " #{name}"
|
|
273
274
|
unless desc.empty?
|
|
@@ -278,6 +279,7 @@ module AIA
|
|
|
278
279
|
puts
|
|
279
280
|
end
|
|
280
281
|
|
|
282
|
+
# :reek:TooManyStatements -- sequential markdown report: header, local tools section, one section per MCP server
|
|
281
283
|
def list_tools_markdown(local_tools, mcp_tool_groups)
|
|
282
284
|
total = local_tools.size + mcp_tool_groups.values.sum(&:size)
|
|
283
285
|
sources = 1 + mcp_tool_groups.size
|
|
@@ -301,8 +303,8 @@ module AIA
|
|
|
301
303
|
end
|
|
302
304
|
|
|
303
305
|
def print_tool_markdown(tool)
|
|
304
|
-
name =
|
|
305
|
-
desc =
|
|
306
|
+
name = ToolIntrospection.tool_name(tool)
|
|
307
|
+
desc = ToolIntrospection.tool_description(tool).strip
|
|
306
308
|
|
|
307
309
|
puts "### `#{name}`"
|
|
308
310
|
puts
|
|
@@ -360,7 +362,13 @@ module AIA
|
|
|
360
362
|
end
|
|
361
363
|
|
|
362
364
|
def load_local_tools(config)
|
|
363
|
-
|
|
365
|
+
require_configured_libs(config)
|
|
366
|
+
require_tool_files(config)
|
|
367
|
+
instantiable_tool_classes
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# :reek:DuplicateMethodCall -- each e.message belongs to a different rescue clause exception; nothing to hoist
|
|
371
|
+
def require_configured_libs(config)
|
|
364
372
|
Array(config.require_libs).each do |lib|
|
|
365
373
|
require lib
|
|
366
374
|
rescue LoadError => e
|
|
@@ -369,20 +377,10 @@ module AIA
|
|
|
369
377
|
rescue StandardError => e
|
|
370
378
|
$stderr.puts "Warning: Error in library '#{lib}': #{e.class} - #{e.message}"
|
|
371
379
|
end
|
|
380
|
+
end
|
|
372
381
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
expanded = File.expand_path(path)
|
|
376
|
-
if File.exist?(expanded)
|
|
377
|
-
require expanded
|
|
378
|
-
else
|
|
379
|
-
$stderr.puts "Warning: Tool file not found: #{path}"
|
|
380
|
-
end
|
|
381
|
-
rescue LoadError, StandardError => e
|
|
382
|
-
$stderr.puts "Warning: Failed to load tool '#{path}': #{e.message}"
|
|
383
|
-
end
|
|
384
|
-
|
|
385
|
-
# Scan ObjectSpace for RubyLLM::Tool subclasses
|
|
382
|
+
# Scan ObjectSpace for RubyLLM::Tool subclasses that can be instantiated.
|
|
383
|
+
def instantiable_tool_classes
|
|
386
384
|
ObjectSpace.each_object(Class).select do |klass|
|
|
387
385
|
next false unless defined?(RubyLLM::Tool) && klass < RubyLLM::Tool
|
|
388
386
|
|
|
@@ -402,65 +400,55 @@ module AIA
|
|
|
402
400
|
result.empty? ? normalized : result
|
|
403
401
|
end
|
|
404
402
|
|
|
405
|
-
#
|
|
403
|
+
# :reek:TooManyStatements -- per-server connect loop with progress prints on each outcome path
|
|
406
404
|
def load_mcp_tools_grouped(config)
|
|
407
405
|
servers = filter_mcp_servers(config)
|
|
408
406
|
return {} if servers.empty?
|
|
409
407
|
|
|
410
408
|
quiet_mcp_logger
|
|
411
409
|
|
|
412
|
-
|
|
413
|
-
|
|
410
|
+
servers.each_with_object({}) do |server, groups|
|
|
411
|
+
server_config = McpServerConfig.from_hash(server)
|
|
412
|
+
$stderr.print "MCP: Connecting to #{server_config.name}..."
|
|
413
|
+
$stderr.flush
|
|
414
414
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
415
|
+
tools = collect_server_tools(server_config)
|
|
416
|
+
if tools
|
|
417
|
+
groups[server_config.name] = tools
|
|
418
|
+
$stderr.puts " #{tools.size} tools"
|
|
419
|
+
else
|
|
420
|
+
$stderr.puts " failed"
|
|
421
|
+
end
|
|
422
|
+
rescue StandardError => e
|
|
423
|
+
$stderr.puts " error: #{e.message}"
|
|
424
|
+
end
|
|
425
|
+
end
|
|
422
426
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
427
|
+
# Connect to one server and return its tools, or nil when the client
|
|
428
|
+
# never comes alive.
|
|
429
|
+
def collect_server_tools(server_config)
|
|
430
|
+
mcp_add_client(server_config)
|
|
431
|
+
client = RubyLLM::MCP.clients[server_config.name]
|
|
432
|
+
client.start
|
|
433
|
+
return nil unless client.alive?
|
|
426
434
|
|
|
427
|
-
|
|
428
|
-
|
|
435
|
+
client.tools rescue []
|
|
436
|
+
end
|
|
429
437
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
client = begin
|
|
435
|
-
RubyLLM::MCP.add_client(
|
|
436
|
-
name: name, transport_type: :stdio,
|
|
437
|
-
config: mcp_config, request_timeout: timeout, start: false
|
|
438
|
-
)
|
|
439
|
-
rescue ArgumentError
|
|
440
|
-
RubyLLM::MCP.add_client(
|
|
441
|
-
name: name, transport_type: :stdio,
|
|
442
|
-
config: mcp_config, start: false
|
|
443
|
-
)
|
|
444
|
-
end
|
|
445
|
-
|
|
446
|
-
client = RubyLLM::MCP.clients[name]
|
|
447
|
-
client.start
|
|
448
|
-
|
|
449
|
-
if client.alive?
|
|
450
|
-
server_tools = client.tools rescue []
|
|
451
|
-
groups[name] = server_tools
|
|
452
|
-
$stderr.puts " #{server_tools.size} tools"
|
|
453
|
-
else
|
|
454
|
-
$stderr.puts " failed"
|
|
455
|
-
end
|
|
456
|
-
rescue StandardError => e
|
|
457
|
-
$stderr.puts " error: #{e.message}"
|
|
458
|
-
end
|
|
459
|
-
# rubocop:enable Metrics/BlockLength
|
|
460
|
-
end
|
|
461
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
438
|
+
def mcp_add_client(server_config)
|
|
439
|
+
mcp_config = { command: server_config.command, args: server_config.args }
|
|
440
|
+
mcp_config[:env] = server_config.env unless server_config.env.empty?
|
|
462
441
|
|
|
463
|
-
|
|
442
|
+
RubyLLM::MCP.add_client(
|
|
443
|
+
name: server_config.name, transport_type: :stdio,
|
|
444
|
+
config: mcp_config, request_timeout: server_config.timeout_ms, start: false
|
|
445
|
+
)
|
|
446
|
+
rescue ArgumentError
|
|
447
|
+
# Older ruby_llm-mcp versions do not accept request_timeout
|
|
448
|
+
RubyLLM::MCP.add_client(
|
|
449
|
+
name: server_config.name, transport_type: :stdio,
|
|
450
|
+
config: mcp_config, start: false
|
|
451
|
+
)
|
|
464
452
|
end
|
|
465
453
|
# rubocop:enable Metrics/ModuleLength
|
|
466
454
|
|
|
@@ -500,9 +488,10 @@ module AIA
|
|
|
500
488
|
end
|
|
501
489
|
|
|
502
490
|
def prepare_pipeline(config)
|
|
503
|
-
|
|
491
|
+
prompt_id = config.prompt_id
|
|
492
|
+
return if prompt_id.nil? || prompt_id.empty? || prompt_id == config.pipeline.first
|
|
504
493
|
|
|
505
|
-
config.pipeline.prepend(
|
|
494
|
+
config.pipeline.prepend(prompt_id)
|
|
506
495
|
end
|
|
507
496
|
|
|
508
497
|
def validate_pipeline_prompts(config)
|
data/lib/aia/config.rb
CHANGED
|
@@ -259,7 +259,7 @@ module AIA
|
|
|
259
259
|
end
|
|
260
260
|
|
|
261
261
|
# Convert config to hash (for dump, etc.)
|
|
262
|
-
# rubocop:disable Metrics/AbcSize
|
|
262
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
263
263
|
def to_h
|
|
264
264
|
{
|
|
265
265
|
service: service.to_h,
|
|
@@ -287,11 +287,12 @@ module AIA
|
|
|
287
287
|
context_files: context_files
|
|
288
288
|
}
|
|
289
289
|
end
|
|
290
|
-
# rubocop:enable Metrics/AbcSize
|
|
291
290
|
|
|
292
291
|
private
|
|
293
292
|
|
|
294
293
|
# Load a config file that REPLACES the user's personal config.
|
|
294
|
+
# :reek:TooManyStatements -- single case dispatch mapping YAML keys to typed config setters; one branch per key family
|
|
295
|
+
# :reek:DuplicateMethodCall -- Array(value) sits in mutually exclusive case branches; hoisting it would coerce hash-section values too
|
|
295
296
|
def load_extra_config(path)
|
|
296
297
|
path = File.expand_path(path)
|
|
297
298
|
|
|
@@ -34,6 +34,7 @@ module AIA
|
|
|
34
34
|
#
|
|
35
35
|
# @param flow_result [SimpleFlow::Result]
|
|
36
36
|
# @return [String]
|
|
37
|
+
# :reek:TooManyStatements -- one pass duck-types reply/content/duration into labeled sections
|
|
37
38
|
def extract_network_content(flow_result)
|
|
38
39
|
parts = []
|
|
39
40
|
flow_result.context.each do |task_name, robot_result|
|
|
@@ -97,6 +98,7 @@ module AIA
|
|
|
97
98
|
# @param tracker [SessionTracker, nil] to record the turn
|
|
98
99
|
# @param decisions [Hash, nil] routing decisions for tracker
|
|
99
100
|
# @return [String] the extracted content
|
|
101
|
+
# :reek:LongParameterList { max_params: 7 } -- presentation seam: each keyword is an independent optional aspect
|
|
100
102
|
def present_result(result, ui_presenter:, streamed_content: nil, prompt: nil, elapsed: nil, tracker: nil, decisions: nil)
|
|
101
103
|
content = streamed_content || extract_content(result)
|
|
102
104
|
|
data/lib/aia/cost_calculator.rb
CHANGED
|
@@ -13,6 +13,7 @@ module AIA
|
|
|
13
13
|
# @param output_tokens [Integer]
|
|
14
14
|
# @return [Hash] { available: true, total_cost:, input_cost:, output_cost: }
|
|
15
15
|
# or { available: false } when pricing is unavailable
|
|
16
|
+
# :reek:TooManyStatements -- linear price lookup: guard, registry fetch, per-direction cost math, result hash
|
|
16
17
|
def self.calculate(model_id:, input_tokens:, output_tokens:)
|
|
17
18
|
return { available: false } unless model_id && defined?(RubyLLM::Models)
|
|
18
19
|
|
data/lib/aia/debate_handler.rb
CHANGED
|
@@ -36,7 +36,7 @@ module AIA
|
|
|
36
36
|
#
|
|
37
37
|
# @param context [HandlerContext] — reads context.prompt
|
|
38
38
|
# @return [String, nil] formatted debate results, or nil if not applicable
|
|
39
|
-
#
|
|
39
|
+
# :reek:TooManyStatements -- debate orchestration: per-round async fan-out, convergence check, tracking; stages share round state
|
|
40
40
|
def handle(context)
|
|
41
41
|
prompt = context.prompt
|
|
42
42
|
return nil unless @robot.network?
|
|
@@ -55,33 +55,19 @@ module AIA
|
|
|
55
55
|
rounds = []
|
|
56
56
|
|
|
57
57
|
MAX_ROUNDS.times do |round|
|
|
58
|
+
round_no = round + 1
|
|
58
59
|
round_context = build_round_context(prompt, rounds)
|
|
59
60
|
|
|
60
|
-
round_results =
|
|
61
|
-
barrier = Async::Barrier.new
|
|
62
|
-
tasks = robots.map do |robot|
|
|
63
|
-
barrier.async do
|
|
64
|
-
@ui_presenter.display_info(" Round #{round + 1}: #{robot.name}...")
|
|
65
|
-
result = robot.run(round_context, mcp: :inherit, tools: :inherit)
|
|
66
|
-
content = extract_content(result)
|
|
67
|
-
write_to_memory(round, robot.name, content)
|
|
68
|
-
{ robot: robot.name, content: content }
|
|
69
|
-
rescue => e
|
|
70
|
-
FailedResponse.new(robot_name: robot.name, error_message: e.message)
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
barrier.wait
|
|
74
|
-
tasks.map(&:wait)
|
|
75
|
-
end
|
|
61
|
+
round_results = run_debate_round(robots, round_context, round, round_no)
|
|
76
62
|
|
|
77
|
-
raise DebateError, "All robots failed in round #{
|
|
63
|
+
raise DebateError, "All robots failed in round #{round_no}" if
|
|
78
64
|
round_results.all?(FailedResponse)
|
|
79
65
|
|
|
80
66
|
previous = rounds.last
|
|
81
67
|
rounds << round_results
|
|
82
68
|
|
|
83
69
|
if converged?(round, round_results, previous)
|
|
84
|
-
@ui_presenter.display_info(" Converged in round #{
|
|
70
|
+
@ui_presenter.display_info(" Converged in round #{round_no}.")
|
|
85
71
|
break
|
|
86
72
|
end
|
|
87
73
|
end
|
|
@@ -94,10 +80,32 @@ module AIA
|
|
|
94
80
|
|
|
95
81
|
format_rounds(rounds)
|
|
96
82
|
end
|
|
97
|
-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
98
83
|
|
|
99
84
|
private
|
|
100
85
|
|
|
86
|
+
# Run every robot once against the round context, concurrently.
|
|
87
|
+
# Returns one entry per robot: a result hash, or FailedResponse on error.
|
|
88
|
+
# :reek:TooManyStatements -- async fan-out with per-robot progress, memory write, and failure capture
|
|
89
|
+
def run_debate_round(robots, round_context, round, round_no)
|
|
90
|
+
Sync do
|
|
91
|
+
barrier = Async::Barrier.new
|
|
92
|
+
tasks = robots.map do |robot|
|
|
93
|
+
barrier.async do
|
|
94
|
+
name = robot.name
|
|
95
|
+
@ui_presenter.display_info(" Round #{round_no}: #{name}...")
|
|
96
|
+
result = robot.run(round_context, mcp: :inherit, tools: :inherit)
|
|
97
|
+
content = extract_content(result)
|
|
98
|
+
write_to_memory(round, name, content)
|
|
99
|
+
{ robot: name, content: content }
|
|
100
|
+
rescue => e
|
|
101
|
+
FailedResponse.new(robot_name: name, error_message: e.message)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
barrier.wait
|
|
105
|
+
tasks.map(&:wait)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
101
109
|
def build_round_context(prompt, rounds)
|
|
102
110
|
return prompt if rounds.empty?
|
|
103
111
|
|
data/lib/aia/delegate_handler.rb
CHANGED
|
@@ -27,6 +27,7 @@ module AIA
|
|
|
27
27
|
#
|
|
28
28
|
# @param context [HandlerContext] — reads context.prompt
|
|
29
29
|
# @return [String, nil] combined results, or nil if not applicable
|
|
30
|
+
# :reek:TooManyStatements -- three-step delegation pipeline (decompose, plan, execute) sharing intermediate results
|
|
30
31
|
def handle(context)
|
|
31
32
|
prompt = context.prompt
|
|
32
33
|
return nil unless @robot.network?
|
|
@@ -71,6 +72,7 @@ module AIA
|
|
|
71
72
|
|
|
72
73
|
private
|
|
73
74
|
|
|
75
|
+
# :reek:TooManyStatements -- per-step loop: resolve assignee, announce, execute, record to memory and results
|
|
74
76
|
def execute_steps(executor, prompt, plan, steps, robots)
|
|
75
77
|
results = []
|
|
76
78
|
|
|
@@ -78,13 +80,15 @@ module AIA
|
|
|
78
80
|
step_def = steps[i]
|
|
79
81
|
assignee = robots.values.find { |r| r.name == step_def[:assignee] }
|
|
80
82
|
assignee ||= robots.values.first
|
|
83
|
+
name = assignee.name
|
|
84
|
+
title = step_def[:title]
|
|
81
85
|
|
|
82
|
-
@ui_presenter.display_info(" #{
|
|
86
|
+
@ui_presenter.display_info(" #{name}: #{title}...")
|
|
83
87
|
|
|
84
88
|
content = executor.execute(task, assignee, step_def, prompt, results)
|
|
85
89
|
|
|
86
|
-
write_to_memory(i,
|
|
87
|
-
results << { robot:
|
|
90
|
+
write_to_memory(i, name, title, content)
|
|
91
|
+
results << { robot: name, task: title, content: content }
|
|
88
92
|
end
|
|
89
93
|
|
|
90
94
|
results
|
data/lib/aia/directive.rb
CHANGED
|
@@ -68,6 +68,7 @@ module AIA
|
|
|
68
68
|
# Application-level concern: formats help text using DIRECTIVE_PREFIX
|
|
69
69
|
# and all registered directive subclasses.
|
|
70
70
|
|
|
71
|
+
# :reek:TooManyStatements -- sequential terminal report: header, one section per directive category, footer
|
|
71
72
|
def help
|
|
72
73
|
puts
|
|
73
74
|
puts "Available Directives"
|
|
@@ -84,14 +85,7 @@ module AIA
|
|
|
84
85
|
puts "-" * cat.length
|
|
85
86
|
|
|
86
87
|
klass.directive_descriptions.each do |method_name, description|
|
|
87
|
-
|
|
88
|
-
alias_text = if aliases.any?
|
|
89
|
-
" (aliases: #{aliases.map { |a| "#{DIRECTIVE_PREFIX}#{a}" }.join(', ')})"
|
|
90
|
-
else
|
|
91
|
-
""
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
puts " #{DIRECTIVE_PREFIX}#{method_name}#{alias_text}"
|
|
88
|
+
puts " #{DIRECTIVE_PREFIX}#{method_name}#{alias_text_for(klass, method_name)}"
|
|
95
89
|
puts " #{description}"
|
|
96
90
|
puts
|
|
97
91
|
|
|
@@ -102,6 +96,14 @@ module AIA
|
|
|
102
96
|
puts "\nTotal: #{total} directives available"
|
|
103
97
|
""
|
|
104
98
|
end
|
|
99
|
+
|
|
100
|
+
# Format the "(aliases: ...)" suffix for a directive, or "" when it has none.
|
|
101
|
+
def alias_text_for(klass, method_name)
|
|
102
|
+
aliases = klass.directive_aliases[method_name] || []
|
|
103
|
+
return "" if aliases.empty?
|
|
104
|
+
|
|
105
|
+
" (aliases: #{aliases.map { |a| "#{DIRECTIVE_PREFIX}#{a}" }.join(', ')})"
|
|
106
|
+
end
|
|
105
107
|
end
|
|
106
108
|
|
|
107
109
|
private
|