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
|
@@ -15,11 +15,13 @@ module AIA
|
|
|
15
15
|
#
|
|
16
16
|
# @param responses [Array<String>] ordered response texts (first is reference)
|
|
17
17
|
# @return [Array<Float, nil>] similarity scores (nil for first, 0.0..1.0 for rest)
|
|
18
|
+
# :reek:TooManyStatements -- guarded scoring: two early-out shapes, TF-IDF fit/transform, pairwise map, rescue fallback
|
|
18
19
|
def self.score(responses)
|
|
19
|
-
|
|
20
|
+
count = responses.size
|
|
21
|
+
return Array.new(count) if count < 2
|
|
20
22
|
|
|
21
23
|
texts = responses.map { |r| r.to_s.strip }
|
|
22
|
-
return Array.new(
|
|
24
|
+
return Array.new(count) if texts.first.empty?
|
|
23
25
|
|
|
24
26
|
tfidf = Classifier::TFIDF.new
|
|
25
27
|
tfidf.fit(texts)
|
|
@@ -33,7 +35,7 @@ module AIA
|
|
|
33
35
|
end
|
|
34
36
|
end
|
|
35
37
|
rescue StandardError
|
|
36
|
-
Array.new(
|
|
38
|
+
Array.new(count)
|
|
37
39
|
end
|
|
38
40
|
end
|
|
39
41
|
end
|
|
@@ -109,30 +109,63 @@ module AIA
|
|
|
109
109
|
false
|
|
110
110
|
end
|
|
111
111
|
|
|
112
|
-
#
|
|
112
|
+
# :reek:TooManyStatements -- benchmark-instrumented pipeline: decompose, concurrent fan-out, synthesis, reporting
|
|
113
113
|
def handle_decomposition(prompt)
|
|
114
114
|
require_relative 'prompt_decomposer'
|
|
115
|
-
|
|
115
|
+
total_t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
116
116
|
|
|
117
117
|
@ui_presenter.display_info("Decomposing prompt into sub-tasks...")
|
|
118
|
-
decomposer
|
|
119
|
-
|
|
120
|
-
subtasks = decomposer.decompose(prompt)
|
|
121
|
-
decompose_dur = Process.clock_gettime(Process::CLOCK_MONOTONIC) - decompose_t0
|
|
118
|
+
decomposer = PromptDecomposer.new(@robot)
|
|
119
|
+
subtasks, decompose_dur = Timing.timed { decomposer.decompose(prompt) }
|
|
122
120
|
|
|
123
121
|
if subtasks.empty?
|
|
124
122
|
@ui_presenter.display_info("Prompt cannot be meaningfully decomposed. Running normally.")
|
|
125
123
|
return false
|
|
126
124
|
end
|
|
127
125
|
|
|
126
|
+
announce_subtasks(subtasks)
|
|
127
|
+
|
|
128
|
+
results, timings, raw_subtasks, parallel_wall = run_and_time_subtasks(subtasks)
|
|
129
|
+
|
|
130
|
+
raise DecomposeError, "All sub-tasks failed" if results.all?(&:nil?)
|
|
131
|
+
|
|
132
|
+
@ui_presenter.display_info("Synthesizing results...")
|
|
133
|
+
final, synth_dur = Timing.timed { decomposer.synthesize(prompt, results.compact) }
|
|
134
|
+
|
|
135
|
+
total_wall = Process.clock_gettime(Process::CLOCK_MONOTONIC) - total_t0
|
|
136
|
+
|
|
137
|
+
display_decompose_benchmark(decompose_dur: decompose_dur, timings: timings,
|
|
138
|
+
synth_dur: synth_dur, parallel_wall: parallel_wall,
|
|
139
|
+
total_wall: total_wall)
|
|
140
|
+
display_decompose_token_metrics(raw_subtasks, final, total_wall)
|
|
141
|
+
|
|
142
|
+
present_result(final, prompt: prompt, ui_presenter: @ui_presenter, tracker: @tracker)
|
|
143
|
+
true
|
|
144
|
+
rescue StandardError => e
|
|
145
|
+
@ui_presenter.display_info("Decomposition failed: #{e.message}. Falling back to normal mode.")
|
|
146
|
+
false
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def announce_subtasks(subtasks)
|
|
128
150
|
@ui_presenter.display_info("Decomposed into #{subtasks.size} sub-tasks:")
|
|
129
151
|
subtasks.each_with_index { |t, i| @ui_presenter.display_info(" #{i + 1}. #{t}") }
|
|
152
|
+
end
|
|
130
153
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
154
|
+
# Fan out sub-tasks concurrently, capturing per-task and wall-clock timing.
|
|
155
|
+
# Returns [results, timings, raw_subtasks, parallel_wall].
|
|
156
|
+
def run_and_time_subtasks(subtasks)
|
|
157
|
+
timings = Array.new(subtasks.size, 0.0)
|
|
158
|
+
raw_subtasks = Array.new(subtasks.size)
|
|
159
|
+
results, parallel_wall = Timing.timed { run_subtasks_concurrently(subtasks, timings, raw_subtasks) }
|
|
160
|
+
[results, timings, raw_subtasks, parallel_wall]
|
|
161
|
+
end
|
|
134
162
|
|
|
135
|
-
|
|
163
|
+
# Run all sub-tasks concurrently, recording per-task timing and raw results
|
|
164
|
+
# into the caller-supplied arrays. Returns extracted contents (nil per failure).
|
|
165
|
+
# :reek:DuplicateMethodCall -- clock_gettime is read at task start and again at each outcome; each read must be "now"
|
|
166
|
+
# :reek:TooManyStatements -- async fan-out with per-task timing capture on both success and failure paths
|
|
167
|
+
def run_subtasks_concurrently(subtasks, timings, raw_subtasks)
|
|
168
|
+
Sync do
|
|
136
169
|
barrier = Async::Barrier.new
|
|
137
170
|
tasks = subtasks.each_with_index.map do |task, i|
|
|
138
171
|
barrier.async do
|
|
@@ -151,21 +184,13 @@ module AIA
|
|
|
151
184
|
barrier.wait
|
|
152
185
|
tasks.map(&:wait)
|
|
153
186
|
end
|
|
154
|
-
|
|
187
|
+
end
|
|
155
188
|
|
|
156
|
-
|
|
189
|
+
# Print the decomposition benchmark table.
|
|
190
|
+
def display_decompose_benchmark(decompose_dur:, timings:, synth_dur:, parallel_wall:, total_wall:)
|
|
157
191
|
serial_est = timings.sum
|
|
158
192
|
parallel_speedup = serial_est / parallel_wall
|
|
159
193
|
|
|
160
|
-
raise DecomposeError, "All sub-tasks failed" if results.all?(&:nil?)
|
|
161
|
-
|
|
162
|
-
@ui_presenter.display_info("Synthesizing results...")
|
|
163
|
-
synth_t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
164
|
-
final = decomposer.synthesize(prompt, results.compact)
|
|
165
|
-
synth_dur = Process.clock_gettime(Process::CLOCK_MONOTONIC) - synth_t0
|
|
166
|
-
|
|
167
|
-
total_wall = Process.clock_gettime(Process::CLOCK_MONOTONIC) - total_start
|
|
168
|
-
|
|
169
194
|
lines = ["", "Benchmark:"]
|
|
170
195
|
lines << " Decompose probe : #{format('%.2f', decompose_dur)}s"
|
|
171
196
|
timings.each_with_index { |t, i| lines << " Sub-task #{i + 1} : #{format('%.2f', t)}s" }
|
|
@@ -175,21 +200,15 @@ module AIA
|
|
|
175
200
|
lines << " Sub-tasks concurrent : #{format('%.2f', parallel_wall)}s (#{format('%.1f', parallel_speedup)}x parallel speedup)"
|
|
176
201
|
lines << " Total flow wall time : #{format('%.2f', total_wall)}s (compare against --tokens Time for a normal run)"
|
|
177
202
|
@ui_presenter.display_info(lines.join("\n"))
|
|
178
|
-
|
|
179
|
-
display_decompose_token_metrics(raw_subtasks, final, total_wall)
|
|
180
|
-
|
|
181
|
-
present_result(final, prompt: prompt, ui_presenter: @ui_presenter, tracker: @tracker)
|
|
182
|
-
true
|
|
183
|
-
rescue StandardError => e
|
|
184
|
-
@ui_presenter.display_info("Decomposition failed: #{e.message}. Falling back to normal mode.")
|
|
185
|
-
false
|
|
186
203
|
end
|
|
187
204
|
|
|
205
|
+
# :reek:TooManyStatements -- ordered eligibility gates (server count, discovery, grouping) before the network run
|
|
188
206
|
def handle_concurrent_mcp(prompt)
|
|
189
|
-
|
|
207
|
+
cfg = AIA.config
|
|
208
|
+
return false unless (cfg.mcp_servers || []).size > 1
|
|
190
209
|
|
|
191
210
|
discovery = MCPDiscovery.new
|
|
192
|
-
relevant = discovery.discover(
|
|
211
|
+
relevant = discovery.discover(cfg)
|
|
193
212
|
return false if relevant.size <= 1
|
|
194
213
|
|
|
195
214
|
grouper = MCPGrouper.new
|
|
@@ -198,7 +217,7 @@ module AIA
|
|
|
198
217
|
|
|
199
218
|
@ui_presenter.display_info("Running concurrent MCP across #{groups.size} server groups...")
|
|
200
219
|
|
|
201
|
-
network = RobotFactory.build_concurrent_mcp_network(
|
|
220
|
+
network = RobotFactory.build_concurrent_mcp_network(cfg, groups)
|
|
202
221
|
result = @ui_presenter.with_spinner("Processing (concurrent)") { network.run(message: prompt) }
|
|
203
222
|
extract_content(result)
|
|
204
223
|
|
|
@@ -270,6 +289,7 @@ module AIA
|
|
|
270
289
|
|
|
271
290
|
# Aggregate token counts from all sub-task raw results plus the synthesis
|
|
272
291
|
# result and display a combined metrics table. Only runs when --tokens is set.
|
|
292
|
+
# :reek:TooManyStatements -- sums token usage across sub-task results plus synthesis, then prints the metrics block
|
|
273
293
|
def display_decompose_token_metrics(raw_subtasks, synthesis_result, total_elapsed)
|
|
274
294
|
return unless AIA.config.flags.tokens
|
|
275
295
|
|
data/lib/aia/speech.rb
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lib/aia/speech.rb
|
|
4
|
+
#
|
|
5
|
+
# Shared text-to-speech behavior for interactive handlers (ChatLoop,
|
|
6
|
+
# MentionRouter). ContentExtractor duck-calls #speak on any includer
|
|
7
|
+
# that responds to it, so both sides route through this one pipeline.
|
|
8
|
+
|
|
9
|
+
module AIA
|
|
10
|
+
module Speech
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# :reek:TooManyStatements -- two-stage TTS pipeline (convert, then play) with spinner and tempfile cleanup
|
|
14
|
+
def speak(content)
|
|
15
|
+
return unless AIA.speak?
|
|
16
|
+
|
|
17
|
+
audio = AIA.config.audio
|
|
18
|
+
command = audio.speak_command || 'say'
|
|
19
|
+
voice = audio.voice
|
|
20
|
+
text = content.to_s
|
|
21
|
+
env = {}
|
|
22
|
+
env['SPEECH_MODEL'] = audio.speech_model if audio.speech_model
|
|
23
|
+
|
|
24
|
+
if command == 'say'
|
|
25
|
+
# Local TTS: say converts and plays in one step
|
|
26
|
+
run_with_spinner("Speaking...") do
|
|
27
|
+
if voice && !voice.to_s.strip.empty?
|
|
28
|
+
system(env, command, '-v', voice, text)
|
|
29
|
+
else
|
|
30
|
+
system(env, command, text)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
else
|
|
34
|
+
# Custom TTS script: stage 2 = convert text → audio file,
|
|
35
|
+
# stage 3 = play the file. AIA passes the output path as $2.
|
|
36
|
+
require 'tempfile'
|
|
37
|
+
tmpfile = Tempfile.new(['aia-tts-', '.mp3'])
|
|
38
|
+
tmpfile.close
|
|
39
|
+
path = tmpfile.path
|
|
40
|
+
begin
|
|
41
|
+
run_with_spinner("Converting to audio...") do
|
|
42
|
+
system(env, command, text, path)
|
|
43
|
+
end
|
|
44
|
+
if File.size?(path)
|
|
45
|
+
run_with_spinner("Speaking...") do
|
|
46
|
+
system('afplay', path)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
ensure
|
|
50
|
+
tmpfile.unlink
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
rescue StandardError => e
|
|
54
|
+
$stderr.puts "Warning: Speech failed: #{e.message}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def run_with_spinner(message)
|
|
58
|
+
spinner = TTY::Spinner.new("[:spinner] #{message}", format: :bouncing_ball, output: $stderr)
|
|
59
|
+
spinner.auto_spin
|
|
60
|
+
begin
|
|
61
|
+
yield
|
|
62
|
+
ensure
|
|
63
|
+
spinner.stop
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -46,6 +46,7 @@ module AIA
|
|
|
46
46
|
# --mcp-use, --mcp-skip, and KBS mcp_activate decisions), then normalizes
|
|
47
47
|
# each server config via MCPConfigNormalizer before handing off to
|
|
48
48
|
# MCPConnectionManager.
|
|
49
|
+
# :reek:TooManyStatements -- MCP wiring sequence: discovery, validation, normalize fallback, connect, absorb, inject
|
|
49
50
|
def connect_mcp_servers(config)
|
|
50
51
|
return if config.flags.no_mcp
|
|
51
52
|
|
data/lib/aia/streaming_runner.rb
CHANGED
|
@@ -28,6 +28,8 @@ module AIA
|
|
|
28
28
|
# @param spinner_message [String] spinner label
|
|
29
29
|
# @param tools [Array<String>, nil] tool names to allow for this turn (nil = all)
|
|
30
30
|
# @return [Array(Object, String, Float)] [result, streamed_content_or_nil, elapsed_seconds]
|
|
31
|
+
# :reek:TooManyStatements -- one streaming turn: spinner lifecycle, chunk-filter closure, robot run with cleanup, timing
|
|
32
|
+
# :reek:DuplicateMethodCall -- @spinner.stop guards three distinct control paths (first chunk, exception, completion)
|
|
31
33
|
def run(robot, prompt, header: "\nAI:\n ", spinner_message: "Processing...", tools: nil)
|
|
32
34
|
@spinner.reset
|
|
33
35
|
@spinner.update(title: spinner_message)
|
|
@@ -107,11 +109,12 @@ module AIA
|
|
|
107
109
|
|
|
108
110
|
max = max_tools
|
|
109
111
|
names = tools_param == :inherit ? robot_tool_names(robot) : tools_param
|
|
110
|
-
|
|
112
|
+
size = names.size
|
|
113
|
+
return tools_param if size <= max
|
|
111
114
|
|
|
112
|
-
dropped =
|
|
115
|
+
dropped = size - max
|
|
113
116
|
$stderr.puts(
|
|
114
|
-
"⚠ Tool list (#{
|
|
117
|
+
"⚠ Tool list (#{size}) exceeds the provider limit of #{max}; " \
|
|
115
118
|
"sending #{max}, dropping #{dropped}. Enable --auto-tool-filter or reduce " \
|
|
116
119
|
"the available tools to control which ones are sent."
|
|
117
120
|
)
|
|
@@ -18,9 +18,10 @@ module AIA
|
|
|
18
18
|
# @param model_spec [ModelSpec, nil] optional model spec with role override
|
|
19
19
|
# @return [String, nil] the assembled system prompt
|
|
20
20
|
def resolve_system_prompt(config, model_spec = nil)
|
|
21
|
-
|
|
21
|
+
prompts_cfg = config.prompts
|
|
22
|
+
system_prompt = prompts_cfg.system_prompt
|
|
22
23
|
|
|
23
|
-
role_id = model_spec&.role ||
|
|
24
|
+
role_id = model_spec&.role || prompts_cfg.role
|
|
24
25
|
if role_id && !role_id.empty?
|
|
25
26
|
role_content = load_role_content(config, role_id)
|
|
26
27
|
if role_content
|
|
@@ -32,7 +33,7 @@ module AIA
|
|
|
32
33
|
# Pipeline mode appends skills to each prompt text instead (pipeline_orchestrator).
|
|
33
34
|
if config.flags&.chat == true
|
|
34
35
|
skill_content = AIA::SkillUtils.load_skills_content(
|
|
35
|
-
Array(
|
|
36
|
+
Array(prompts_cfg&.skills),
|
|
36
37
|
AIA::SkillUtils.skills_base_dir(config)
|
|
37
38
|
)
|
|
38
39
|
system_prompt = [system_prompt, skill_content].compact.join("\n\n") if skill_content
|
|
@@ -48,6 +49,7 @@ module AIA
|
|
|
48
49
|
# @param spec [ModelSpec] this robot's model spec
|
|
49
50
|
# @param roster [Array<Hash>] all robots: [{ name:, spec: }, ...]
|
|
50
51
|
# @return [String]
|
|
52
|
+
# :reek:TooManyStatements -- builds the identity line plus a roster entry per teammate; one cohesive prompt fragment
|
|
51
53
|
def build_identity_prompt(robot_name, spec, roster)
|
|
52
54
|
provider_label = spec.provider ? " (#{spec.provider})" : ""
|
|
53
55
|
lines = ["You are #{robot_name}, powered by #{spec.name}#{provider_label}."]
|
|
@@ -55,9 +57,10 @@ module AIA
|
|
|
55
57
|
if roster.size > 1
|
|
56
58
|
lines << "You are part of a team of AI robots:"
|
|
57
59
|
roster.each do |entry|
|
|
58
|
-
|
|
60
|
+
entry_spec = entry[:spec]
|
|
61
|
+
p = entry_spec.provider ? " (#{entry_spec.provider})" : ""
|
|
59
62
|
marker = entry[:name] == robot_name ? " ← you" : ""
|
|
60
|
-
lines << " - #{entry[:name]}: #{
|
|
63
|
+
lines << " - #{entry[:name]}: #{entry_spec.name}#{p}#{marker}"
|
|
61
64
|
end
|
|
62
65
|
lines << "Users can address a specific robot with @name mentions."
|
|
63
66
|
end
|
|
@@ -71,12 +74,13 @@ module AIA
|
|
|
71
74
|
# @param role_id [String] the role identifier
|
|
72
75
|
# @return [String, nil] the role file content or nil
|
|
73
76
|
def load_role_content(config, role_id)
|
|
74
|
-
|
|
77
|
+
prompts_cfg = config.prompts
|
|
78
|
+
roles_prefix = prompts_cfg.roles_prefix
|
|
75
79
|
unless role_id.start_with?(roles_prefix)
|
|
76
80
|
role_id = "#{roles_prefix}/#{role_id}"
|
|
77
81
|
end
|
|
78
82
|
|
|
79
|
-
role_file = File.join(
|
|
83
|
+
role_file = File.join(prompts_cfg.dir, "#{role_id}#{prompts_cfg.extname}")
|
|
80
84
|
return nil unless File.exist?(role_file)
|
|
81
85
|
|
|
82
86
|
File.read(role_file)
|
data/lib/aia/task_coordinator.rb
CHANGED
|
@@ -33,6 +33,7 @@ module AIA
|
|
|
33
33
|
# @param labels [Array<String>] labels like "robot:alice", "domain:code"
|
|
34
34
|
# @param creator [String] name of the creating robot
|
|
35
35
|
# @return [TrakFlow::Models::Task, nil]
|
|
36
|
+
# :reek:LongParameterList { max_params: 6 } -- one keyword per optional task attribute; callers set only what they need
|
|
36
37
|
def create_task(title, assignee: nil, parent_id: nil,
|
|
37
38
|
blocked_by: [], labels: [], creator: "aia")
|
|
38
39
|
return nil unless available?
|
|
@@ -40,27 +41,28 @@ module AIA
|
|
|
40
41
|
task = TrakFlow::Models::Task.new(
|
|
41
42
|
title: title, assignee: assignee, type: "task"
|
|
42
43
|
)
|
|
43
|
-
task
|
|
44
|
+
task = @db.create_task(task)
|
|
45
|
+
task_id = task.id
|
|
44
46
|
|
|
45
47
|
@db.add_label(TrakFlow::Models::Label.new(
|
|
46
|
-
task_id:
|
|
48
|
+
task_id: task_id, name: "creator:#{creator}"
|
|
47
49
|
))
|
|
48
50
|
|
|
49
51
|
labels.each do |label|
|
|
50
52
|
@db.add_label(TrakFlow::Models::Label.new(
|
|
51
|
-
task_id:
|
|
53
|
+
task_id: task_id, name: label
|
|
52
54
|
))
|
|
53
55
|
end
|
|
54
56
|
|
|
55
57
|
blocked_by.each do |blocker_id|
|
|
56
58
|
@db.add_dependency(TrakFlow::Models::Dependency.new(
|
|
57
|
-
source_id: blocker_id, target_id:
|
|
59
|
+
source_id: blocker_id, target_id: task_id, type: "blocks"
|
|
58
60
|
))
|
|
59
61
|
end
|
|
60
62
|
|
|
61
63
|
if parent_id
|
|
62
64
|
@db.add_dependency(TrakFlow::Models::Dependency.new(
|
|
63
|
-
source_id: parent_id, target_id:
|
|
65
|
+
source_id: parent_id, target_id: task_id, type: "parent-child"
|
|
64
66
|
))
|
|
65
67
|
end
|
|
66
68
|
|
|
@@ -74,6 +76,8 @@ module AIA
|
|
|
74
76
|
# @param creator [String] creating robot name
|
|
75
77
|
# @param ephemeral [Boolean] true for single-session plans (auto-gc)
|
|
76
78
|
# @return [Hash, nil] { plan:, steps: }
|
|
79
|
+
# :reek:BooleanParameter -- ephemeral: tags a plan for auto-gc at session end; a second method would duplicate the whole build
|
|
80
|
+
# :reek:TooManyStatements -- plan build: create the plan task, then per step a child task, labels, and ordering dependency
|
|
77
81
|
def create_plan(title, steps:, creator: "aia", ephemeral: false)
|
|
78
82
|
return nil unless available?
|
|
79
83
|
|
|
@@ -177,6 +181,7 @@ module AIA
|
|
|
177
181
|
# Summary of the current task board state.
|
|
178
182
|
#
|
|
179
183
|
# @return [String, nil]
|
|
184
|
+
# :reek:TooManyStatements -- one board report: totals line, per-assignee open/done counts, unassigned tail
|
|
180
185
|
def status_summary
|
|
181
186
|
return nil unless available?
|
|
182
187
|
|
data/lib/aia/timing.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# lib/aia/timing.rb
|
|
2
|
+
|
|
3
|
+
module AIA
|
|
4
|
+
# Monotonic-clock instrumentation shared by benchmark-reporting code.
|
|
5
|
+
module Timing
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# Run the block and return [result, elapsed_seconds].
|
|
9
|
+
def timed
|
|
10
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
11
|
+
result = yield
|
|
12
|
+
[result, Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0]
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/aia/tool_filter.rb
CHANGED
|
@@ -13,6 +13,7 @@ module AIA
|
|
|
13
13
|
class ToolFilter
|
|
14
14
|
attr_reader :tool_count, :prep_ms, :label
|
|
15
15
|
|
|
16
|
+
# :reek:BooleanParameter -- load_db:/save_db: toggle embedding-cache persistence per strategy; mode objects would multiply constructors
|
|
16
17
|
def initialize(label:, db_dir: nil, load_db: false, save_db: false)
|
|
17
18
|
@label = label
|
|
18
19
|
@db_dir = db_dir
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
# After each resolve(), prints a timing table showing
|
|
9
9
|
# prep and filter times for the active filter.
|
|
10
10
|
|
|
11
|
+
require 'tty-table'
|
|
12
|
+
|
|
11
13
|
module AIA
|
|
12
14
|
class ToolFilterStrategy
|
|
13
15
|
# Maps filter keys to display info.
|
|
@@ -88,26 +90,45 @@ module AIA
|
|
|
88
90
|
|
|
89
91
|
# Display single-filter results via logger.
|
|
90
92
|
def display_filter_results(key, scored)
|
|
91
|
-
meta
|
|
93
|
+
meta = meta_for(key)
|
|
94
|
+
label = meta[:label]
|
|
95
|
+
logger = AIA.logger
|
|
92
96
|
|
|
93
97
|
if scored.empty?
|
|
94
|
-
|
|
98
|
+
logger.debug "[#{label}] No tools matched (all tools available)"
|
|
95
99
|
return
|
|
96
100
|
end
|
|
97
101
|
|
|
98
102
|
names = scored.map { |e| e[:name] }.join(', ')
|
|
99
|
-
|
|
103
|
+
logger.debug "[#{label}] Tools for this turn (#{scored.size}): #{names}"
|
|
100
104
|
|
|
101
105
|
scored.each do |entry|
|
|
102
|
-
|
|
106
|
+
logger.debug "[#{label}] #{entry[:name]} (#{meta[:score_label]}: #{format('%.4f', entry[:score])})"
|
|
103
107
|
end
|
|
104
108
|
end
|
|
105
109
|
|
|
106
110
|
# Print the timing table via logger.
|
|
107
|
-
#
|
|
111
|
+
# :reek:DuplicateMethodCall -- header and each data row map the same columns into different cells
|
|
108
112
|
def display_timing_table(filter_ms_by_key)
|
|
109
|
-
columns =
|
|
110
|
-
|
|
113
|
+
columns = build_timing_columns(filter_ms_by_key)
|
|
114
|
+
return if columns.empty?
|
|
115
|
+
|
|
116
|
+
header = ["Process"] + columns.map { |col| "#{col[:header]}\n#{col[:sub]}" }
|
|
117
|
+
rows = [
|
|
118
|
+
["prep"] + columns.map { |col| col[:prep] },
|
|
119
|
+
["filter"] + columns.map { |col| col[:filter] }
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
alignments = [:left] + Array.new(columns.size, :right)
|
|
123
|
+
rendered = TTY::Table.new(header, rows)
|
|
124
|
+
.render(:unicode, multiline: true, alignments: alignments, padding: [0, 1])
|
|
125
|
+
rendered.each_line { |line| AIA.logger.debug(line.chomp) }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Column data (header, sub-label, prep/filter cells) for each configured filter.
|
|
129
|
+
def build_timing_columns(filter_ms_by_key)
|
|
130
|
+
@filters.map do |key, filter|
|
|
131
|
+
meta = meta_for(key)
|
|
111
132
|
active = filter_ms_by_key.key?(key)
|
|
112
133
|
{
|
|
113
134
|
header: "Option #{meta[:letter]}",
|
|
@@ -116,25 +137,7 @@ module AIA
|
|
|
116
137
|
filter: active ? fmt_ms(filter_ms_by_key[key]) : "--"
|
|
117
138
|
}
|
|
118
139
|
end
|
|
119
|
-
|
|
120
|
-
return if columns.empty?
|
|
121
|
-
|
|
122
|
-
pw = 7 # "Process" column width
|
|
123
|
-
widths = columns.map do |col|
|
|
124
|
-
[10, col[:header].length, col[:sub].length, col[:prep].length, col[:filter].length].max
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
lines = []
|
|
128
|
-
lines << ("┌─#{'─' * pw}─" + columns.each_with_index.map { |_, i| "┬─#{'─' * widths[i]}─" }.join + "┐")
|
|
129
|
-
lines << ("│ #{'Process'.ljust(pw)} " + columns.each_with_index.map { |col, i| "│ #{col[:header].ljust(widths[i])} " }.join + "│")
|
|
130
|
-
lines << ("│ #{' ' * pw} " + columns.each_with_index.map { |col, i| "│ #{col[:sub].ljust(widths[i])} " }.join + "│")
|
|
131
|
-
lines << ("├─#{'─' * pw}─" + columns.each_with_index.map { |_, i| "┼─#{'─' * widths[i]}─" }.join + "┤")
|
|
132
|
-
lines << ("│ #{'prep'.ljust(pw)} " + columns.each_with_index.map { |col, i| "│ #{col[:prep].rjust(widths[i])} " }.join + "│")
|
|
133
|
-
lines << ("│ #{'filter'.ljust(pw)} " + columns.each_with_index.map { |col, i| "│ #{col[:filter].rjust(widths[i])} " }.join + "│")
|
|
134
|
-
lines << ("└─#{'─' * pw}─" + columns.each_with_index.map { |_, i| "┴─#{'─' * widths[i]}─" }.join + "┘")
|
|
135
|
-
lines.each { |line| AIA.logger.debug line }
|
|
136
140
|
end
|
|
137
|
-
# rubocop:enable Metrics/AbcSize
|
|
138
141
|
|
|
139
142
|
def fmt_ms(ms)
|
|
140
143
|
"#{ms.round(1)}ms"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# lib/aia/tool_introspection.rb
|
|
2
|
+
|
|
3
|
+
module AIA
|
|
4
|
+
# Shared name/description extraction for tool objects, which may be
|
|
5
|
+
# RubyLLM tools, RobotLab tools, or bare tool classes.
|
|
6
|
+
module ToolIntrospection
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def tool_name(tool)
|
|
10
|
+
tool.respond_to?(:name) ? tool.name : tool.class.name
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def tool_description(tool)
|
|
14
|
+
tool.respond_to?(:description) ? tool.description.to_s : ''
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/aia/tool_loader.rb
CHANGED
|
@@ -53,6 +53,7 @@ module AIA
|
|
|
53
53
|
|
|
54
54
|
# Load tools from require_libs and tool paths, then cache the result.
|
|
55
55
|
# Subsequent calls to RobotFactory.build skip this entirely if cache exists.
|
|
56
|
+
# :reek:TooManyStatements -- sequential phases: require_libs with activation retry, tool files, eager loads, discovery cache
|
|
56
57
|
def load_tools(config)
|
|
57
58
|
Array(config.require_libs).each do |lib|
|
|
58
59
|
require lib
|
|
@@ -89,11 +90,17 @@ module AIA
|
|
|
89
90
|
tools = discover_tools
|
|
90
91
|
@tool_cache = tools
|
|
91
92
|
config.loaded_tools = tools
|
|
92
|
-
config.tool_names = tools.map { |t| t
|
|
93
|
+
config.tool_names = tools.map { |t| tool_name(t) }.join(', ')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# A tool's display name: instances expose #name; bare classes fall back to the class name.
|
|
97
|
+
def tool_name(tool)
|
|
98
|
+
tool.respond_to?(:name) ? tool.name : tool.class.name
|
|
93
99
|
end
|
|
94
100
|
|
|
95
101
|
# Filter tools based on allowed/rejected lists and KBS decisions.
|
|
96
|
-
#
|
|
102
|
+
# :reek:TooManyStatements -- allowed filter, rejected filter, then a name-dedup pass over the tool list
|
|
103
|
+
# :reek:DuplicateMethodCall -- tool_name(t) is applied per element inside three separate filter blocks; no shared scope to hoist into
|
|
97
104
|
def filtered_tools(config)
|
|
98
105
|
tools = config.loaded_tools || []
|
|
99
106
|
allowed = config.tools&.allowed
|
|
@@ -102,7 +109,7 @@ module AIA
|
|
|
102
109
|
if allowed && !allowed.empty?
|
|
103
110
|
allowed_list = Array(allowed).map(&:strip).map(&:downcase)
|
|
104
111
|
tools = tools.select do |t|
|
|
105
|
-
name = (t
|
|
112
|
+
name = tool_name(t).downcase
|
|
106
113
|
# name is a String; Array#intersect? would raise TypeError
|
|
107
114
|
allowed_list.any? { |a| name.include?(a) }
|
|
108
115
|
end
|
|
@@ -111,7 +118,7 @@ module AIA
|
|
|
111
118
|
if rejected && !rejected.empty?
|
|
112
119
|
rejected_list = Array(rejected).map(&:strip).map(&:downcase)
|
|
113
120
|
tools = tools.reject do |t|
|
|
114
|
-
name = (t
|
|
121
|
+
name = tool_name(t).downcase
|
|
115
122
|
# name is a String; Array#intersect? would raise TypeError
|
|
116
123
|
rejected_list.any? { |r| name.include?(r) }
|
|
117
124
|
end
|
|
@@ -119,7 +126,7 @@ module AIA
|
|
|
119
126
|
|
|
120
127
|
seen = {}
|
|
121
128
|
tools.select do |t|
|
|
122
|
-
name = t
|
|
129
|
+
name = tool_name(t)
|
|
123
130
|
if seen[name]
|
|
124
131
|
false
|
|
125
132
|
else
|
|
@@ -128,7 +135,6 @@ module AIA
|
|
|
128
135
|
end
|
|
129
136
|
end
|
|
130
137
|
end
|
|
131
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
132
138
|
|
|
133
139
|
# Discover RubyLLM::Tool subclasses from ObjectSpace.
|
|
134
140
|
# Skips tools that report themselves as unavailable via #available?.
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
require "json"
|
|
10
10
|
|
|
11
11
|
module AIA
|
|
12
|
+
# :reek:DataClump -- each execute_<action> dispatch target receives the same LLM tool-call context
|
|
12
13
|
class TaskBoardTool < RubyLLM::Tool
|
|
13
14
|
description "Manage a shared task board. Create tasks for other robots, " \
|
|
14
15
|
"check what's ready, claim tasks, and report completion."
|
data/lib/aia/trakflow_bridge.rb
CHANGED
|
@@ -26,6 +26,7 @@ module AIA
|
|
|
26
26
|
#
|
|
27
27
|
# @param pipeline [Array<String>] prompt IDs in pipeline order
|
|
28
28
|
# @return [String, nil] summary of created plan
|
|
29
|
+
# :reek:TooManyStatements -- plan build: one child task per pipeline step, chained by blocking dependencies
|
|
29
30
|
def create_plan_from_pipeline(pipeline)
|
|
30
31
|
return nil unless available?
|
|
31
32
|
|
|
@@ -71,15 +72,16 @@ module AIA
|
|
|
71
72
|
case status
|
|
72
73
|
when :started
|
|
73
74
|
task.status = "in_progress"
|
|
74
|
-
@db.update_task(task)
|
|
75
75
|
when :completed
|
|
76
76
|
task.close!(reason: reason || "Completed")
|
|
77
|
-
@db.update_task(task)
|
|
78
77
|
when :failed
|
|
79
78
|
task.status = "blocked"
|
|
80
79
|
task.append_trace("blocked", reason) if reason
|
|
81
|
-
|
|
80
|
+
else
|
|
81
|
+
return
|
|
82
82
|
end
|
|
83
|
+
|
|
84
|
+
@db.update_task(task)
|
|
83
85
|
rescue StandardError => e
|
|
84
86
|
$stderr.puts "Warning: TrakFlow status update failed: #{e.message}"
|
|
85
87
|
end
|
data/lib/aia/turn_state.rb
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
# and direct test assertions).
|
|
13
13
|
|
|
14
14
|
module AIA
|
|
15
|
+
# :reek:TooManyInstanceVariables -- one boolean flag per exclusive mode plus spawn/mcp details; a Hash would lose the attr_accessor API
|
|
15
16
|
class TurnState
|
|
16
17
|
# Exclusive mode flags — only one may be active at a time via #request.
|
|
17
18
|
EXCLUSIVE_MODES = %i[
|