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,13 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
require 'faraday'
|
|
4
4
|
require 'clipboard'
|
|
5
|
-
require 'yaml'
|
|
6
|
-
require 'io/console'
|
|
7
|
-
require 'word_wrapper'
|
|
8
5
|
|
|
9
6
|
module AIA
|
|
10
7
|
class WebAndFileDirectives < Directive
|
|
11
8
|
include AIA::SkillUtils
|
|
9
|
+
|
|
12
10
|
PUREMD_API_KEY = ENV.fetch('PUREMD_API_KEY', nil)
|
|
13
11
|
|
|
14
12
|
desc "Fetch and include content from a webpage"
|
|
@@ -23,20 +21,20 @@ module AIA
|
|
|
23
21
|
req.headers['x-puremd-api-token'] = PUREMD_API_KEY
|
|
24
22
|
end
|
|
25
23
|
|
|
26
|
-
if
|
|
24
|
+
if response.status == 200
|
|
27
25
|
response.body
|
|
28
26
|
else
|
|
29
27
|
"Error: Status was #{response.status}\n#{ap response}"
|
|
30
28
|
end
|
|
31
29
|
end
|
|
32
30
|
end
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
alias website webpage
|
|
32
|
+
alias web webpage
|
|
35
33
|
|
|
36
34
|
desc "List available AIA skills"
|
|
35
|
+
# rubocop:disable Metrics/AbcSize
|
|
37
36
|
def skills(args = [], _context_manager = nil)
|
|
38
37
|
dir = aia_skills_dir
|
|
39
|
-
|
|
40
38
|
unless Dir.exist?(dir)
|
|
41
39
|
puts "No skills directory found at #{dir}"
|
|
42
40
|
return nil
|
|
@@ -44,74 +42,56 @@ module AIA
|
|
|
44
42
|
|
|
45
43
|
positive_terms, negative_terms = parse_search_terms(Array(args))
|
|
46
44
|
|
|
47
|
-
skill_dirs = Dir.children(dir)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if skill_dirs.empty?
|
|
52
|
-
puts "No skills found in #{dir}"
|
|
53
|
-
return nil
|
|
45
|
+
skill_dirs = Dir.children(dir).select do |e|
|
|
46
|
+
File.directory?(File.join(dir, e)) &&
|
|
47
|
+
File.exist?(File.join(dir, e, 'SKILL.md'))
|
|
54
48
|
end
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
text
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
puts "No skills
|
|
49
|
+
entries = skill_dirs.select do |e|
|
|
50
|
+
next true if positive_terms.empty? && negative_terms.empty?
|
|
51
|
+
text = read_front_matter_text(File.join(dir, e, 'SKILL.md'))
|
|
52
|
+
# text is a String; Array#intersect? would raise TypeError
|
|
53
|
+
positive_terms.all? { |t| text.include?(t) } &&
|
|
54
|
+
negative_terms.none? { |t| text.include?(t) }
|
|
55
|
+
end.sort
|
|
56
|
+
|
|
57
|
+
if entries.empty?
|
|
58
|
+
all_terms = positive_terms + negative_terms
|
|
59
|
+
puts all_terms.empty? ? "No skills found in #{dir}" : "No skills matched: #{Array(args).join(' ')}"
|
|
66
60
|
return nil
|
|
67
61
|
end
|
|
68
62
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
puts
|
|
77
|
-
|
|
78
|
-
puts WordWrapper::MinimumRaggedness.new(width - 4, desc_text).wrap
|
|
79
|
-
.split("\n").map { |l| " #{l}" }.join("\n")
|
|
80
|
-
end
|
|
63
|
+
wrap_width = terminal_width - 2
|
|
64
|
+
|
|
65
|
+
entries.each do |skill_id|
|
|
66
|
+
fm = parse_skill_front_matter(File.join(dir, skill_id, 'SKILL.md'))
|
|
67
|
+
name = fm['name'] || ''
|
|
68
|
+
description = fm['description'] || '(no description)'
|
|
69
|
+
puts "#{skill_id}: #{name}"
|
|
70
|
+
puts word_wrap(description, width: wrap_width, indent: ' ')
|
|
71
|
+
puts
|
|
81
72
|
end
|
|
82
73
|
|
|
83
|
-
puts "\nTotal: #{skill_data.size} skill#{'s' if skill_data.size != 1}"
|
|
84
74
|
nil
|
|
85
75
|
end
|
|
76
|
+
# rubocop:enable Metrics/AbcSize
|
|
86
77
|
|
|
87
|
-
desc "Include an AIA skill from the skills directory"
|
|
78
|
+
desc "Include an AIA skill from the configured skills directory"
|
|
88
79
|
def skill(args = [], _context_manager = nil)
|
|
89
80
|
args = Array(args)
|
|
90
81
|
skill_name = args.first&.strip
|
|
91
|
-
|
|
92
82
|
if skill_name.nil? || skill_name.empty?
|
|
93
|
-
|
|
94
|
-
AIA::LoggerManager.aia_logger.error(
|
|
83
|
+
msg = "Error: /skill requires a skill name. Use /skills to list available skills."
|
|
84
|
+
AIA::LoggerManager.aia_logger.error(msg)
|
|
85
|
+
puts msg
|
|
95
86
|
return nil
|
|
96
87
|
end
|
|
97
88
|
|
|
98
89
|
dir = aia_skills_dir
|
|
99
|
-
|
|
100
|
-
unless Dir.exist?(dir)
|
|
101
|
-
warn "Error: Skills directory not found at #{dir}"
|
|
102
|
-
AIA::LoggerManager.aia_logger.error("Skills directory not found at #{dir}")
|
|
103
|
-
return nil
|
|
104
|
-
end
|
|
105
|
-
|
|
106
90
|
skill_dir = find_skill_dir(skill_name, dir)
|
|
107
91
|
unless skill_dir
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
else
|
|
112
|
-
warn "Error: No skill matching '#{skill_name}' found in #{dir}. Use /skills to list available skills."
|
|
113
|
-
AIA::LoggerManager.aia_logger.error("No skill matching '#{skill_name}' found in #{dir}")
|
|
114
|
-
end
|
|
92
|
+
msg = "Error: No skill matching '#{skill_name}' found in #{dir}. Use /skills to list available skills."
|
|
93
|
+
AIA::LoggerManager.aia_logger.error(msg)
|
|
94
|
+
puts msg
|
|
115
95
|
return nil
|
|
116
96
|
end
|
|
117
97
|
|
|
@@ -119,8 +99,13 @@ module AIA
|
|
|
119
99
|
|
|
120
100
|
skill_path = File.join(skill_dir, 'SKILL.md')
|
|
121
101
|
unless File.exist?(skill_path)
|
|
122
|
-
|
|
123
|
-
|
|
102
|
+
msg = if path_based_id?(skill_name)
|
|
103
|
+
"Error: No skill matching '#{skill_name}' found in #{dir}. Use /skills to list available skills."
|
|
104
|
+
else
|
|
105
|
+
"Error: Skill '#{File.basename(skill_dir)}' has no SKILL.md in #{dir}."
|
|
106
|
+
end
|
|
107
|
+
AIA::LoggerManager.aia_logger.error(msg)
|
|
108
|
+
puts msg
|
|
124
109
|
return nil
|
|
125
110
|
end
|
|
126
111
|
|
|
@@ -134,19 +119,70 @@ module AIA
|
|
|
134
119
|
rescue StandardError => e
|
|
135
120
|
"Error: Unable to paste from clipboard - #{e.message}"
|
|
136
121
|
end
|
|
137
|
-
|
|
122
|
+
alias clipboard paste
|
|
138
123
|
|
|
139
124
|
private
|
|
140
125
|
|
|
126
|
+
# Resolve the AIA skills directory from config, env vars, or defaults.
|
|
141
127
|
def aia_skills_dir
|
|
142
|
-
AIA.config.skills.dir
|
|
128
|
+
if AIA.respond_to?(:config) && AIA.config&.skills.respond_to?(:dir) && AIA.config.skills.dir
|
|
129
|
+
return AIA.config.skills.dir
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
ENV.fetch('AIA_SKILLS__DIR', File.expand_path('~/.prompts/skills'))
|
|
143
133
|
end
|
|
144
134
|
|
|
145
135
|
def terminal_width
|
|
136
|
+
require 'io/console'
|
|
146
137
|
IO.console&.winsize&.last || 80
|
|
147
138
|
rescue StandardError
|
|
148
139
|
80
|
|
149
140
|
end
|
|
150
141
|
|
|
142
|
+
# Word-wrap text to width, prefixing every line with indent.
|
|
143
|
+
def word_wrap(text, width:, indent: '')
|
|
144
|
+
return "#{indent}#{text}" if text.length <= width
|
|
145
|
+
|
|
146
|
+
words = text.split
|
|
147
|
+
lines = []
|
|
148
|
+
line = +''
|
|
149
|
+
|
|
150
|
+
words.each do |word|
|
|
151
|
+
if line.empty?
|
|
152
|
+
line = word
|
|
153
|
+
elsif line.length + 1 + word.length <= width
|
|
154
|
+
line << ' ' << word
|
|
155
|
+
else
|
|
156
|
+
lines << line
|
|
157
|
+
line = word
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
lines << line unless line.empty?
|
|
161
|
+
|
|
162
|
+
lines.map { |l| "#{indent}#{l}" }.join("\n")
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def read_front_matter_text(skill_md_path)
|
|
166
|
+
content = File.read(skill_md_path)
|
|
167
|
+
return '' unless content.start_with?("---")
|
|
168
|
+
|
|
169
|
+
end_pos = content.index("---", 3)
|
|
170
|
+
return '' unless end_pos
|
|
171
|
+
|
|
172
|
+
content[3...end_pos].downcase
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def parse_skill_front_matter(skill_md_path)
|
|
176
|
+
require 'yaml'
|
|
177
|
+
content = File.read(skill_md_path)
|
|
178
|
+
return {} unless content.start_with?("---")
|
|
179
|
+
|
|
180
|
+
end_pos = content.index("---", 3)
|
|
181
|
+
return {} unless end_pos
|
|
182
|
+
|
|
183
|
+
yaml_text = content[3...end_pos].strip
|
|
184
|
+
parsed = YAML.safe_load(yaml_text, symbolize_names: false) rescue {}
|
|
185
|
+
parsed.is_a?(Hash) ? parsed : {}
|
|
186
|
+
end
|
|
151
187
|
end
|
|
152
188
|
end
|
data/lib/aia/errors.rb
CHANGED
|
@@ -21,4 +21,19 @@ module AIA
|
|
|
21
21
|
|
|
22
22
|
# Raised for directive parsing or execution failures
|
|
23
23
|
class DirectiveError < Error; end
|
|
24
|
+
|
|
25
|
+
# Raised for crew recruit/drop failures
|
|
26
|
+
class CrewError < Error; end
|
|
27
|
+
|
|
28
|
+
# Raised when a quality gate blocks prompt execution
|
|
29
|
+
class GateError < Error; end
|
|
30
|
+
|
|
31
|
+
# Raised when all lead agents fail in LayeredOrchestrator
|
|
32
|
+
class OrchestratorError < Error; end
|
|
33
|
+
|
|
34
|
+
# Raised when all robots fail in a single DebateHandler round
|
|
35
|
+
class DebateError < Error; end
|
|
36
|
+
|
|
37
|
+
# Raised when all sub-tasks fail in SpecialModeHandler decompose mode
|
|
38
|
+
class DecomposeError < Error; end
|
|
24
39
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lib/aia/fact_asserter.rb
|
|
4
|
+
#
|
|
5
|
+
# Thin adapter that extracts tool_name and tool_description from a tool object.
|
|
6
|
+
# Replaces the KBS FactAsserter with a plain Ruby implementation that reads
|
|
7
|
+
# the standard .name and .description interface common to all AIA tool objects.
|
|
8
|
+
|
|
9
|
+
module AIA
|
|
10
|
+
class FactAsserter
|
|
11
|
+
# Extract the tool's name as a non-nil String.
|
|
12
|
+
#
|
|
13
|
+
# @param tool [Object] any object that responds to .name
|
|
14
|
+
# @return [String]
|
|
15
|
+
def tool_name(tool)
|
|
16
|
+
String(tool.respond_to?(:name) ? tool.name : "").strip
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Extract the tool's description as a non-nil String.
|
|
20
|
+
#
|
|
21
|
+
# @param tool [Object] any object that responds to .description
|
|
22
|
+
# @return [String]
|
|
23
|
+
def tool_description(tool)
|
|
24
|
+
String(tool.respond_to?(:description) ? tool.description : "").strip
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/aia/fzf.rb
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
require 'shellwords'
|
|
5
5
|
require 'open3'
|
|
6
|
-
require 'tempfile'
|
|
7
6
|
|
|
8
7
|
class AIA::Fzf
|
|
9
8
|
DEFAULT_PARAMETERS = %w[
|
|
@@ -12,19 +11,18 @@ class AIA::Fzf
|
|
|
12
11
|
--prompt='Search term: '
|
|
13
12
|
--delimiter :
|
|
14
13
|
--preview-window=down:50%:wrap
|
|
15
|
-
]
|
|
14
|
+
].freeze
|
|
16
15
|
|
|
17
16
|
attr_reader :list, :directory, :query, :subject, :prompt, :extension, :command
|
|
18
17
|
|
|
19
18
|
def initialize(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
list:, # Array of Strings (basenames of files w/o extension)
|
|
20
|
+
directory:, # Parent directory of the list items
|
|
21
|
+
query: '', # String, the thing be searched for
|
|
22
|
+
subject: 'Prompt IDs', # or 'Role Names'
|
|
23
|
+
prompt: 'Select one:',
|
|
24
|
+
extension: '.md'
|
|
25
|
+
)
|
|
28
26
|
@list = list
|
|
29
27
|
@directory = directory
|
|
30
28
|
@query = query
|
|
@@ -35,7 +33,6 @@ class AIA::Fzf
|
|
|
35
33
|
build_command
|
|
36
34
|
end
|
|
37
35
|
|
|
38
|
-
|
|
39
36
|
def build_command
|
|
40
37
|
fzf_options = DEFAULT_PARAMETERS.dup
|
|
41
38
|
escaped_dir = Shellwords.escape(directory)
|
|
@@ -47,29 +44,10 @@ class AIA::Fzf
|
|
|
47
44
|
@fzf_args = fzf_options
|
|
48
45
|
end
|
|
49
46
|
|
|
50
|
-
|
|
51
47
|
def run
|
|
52
48
|
input = list.join("\n")
|
|
53
|
-
selected,
|
|
54
|
-
|
|
49
|
+
selected, _status = Open3.capture2('fzf', *@fzf_args, stdin_data: input)
|
|
55
50
|
selected.strip.empty? ? nil : selected.strip
|
|
56
|
-
ensure
|
|
57
|
-
unlink_tempfile
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
##############################################
|
|
61
|
-
private
|
|
62
|
-
|
|
63
|
-
def tempfile_path
|
|
64
|
-
@tempfile ||= Tempfile.new('fzf-input').tap do |file|
|
|
65
|
-
list.each { |item| file.puts item }
|
|
66
|
-
file.close
|
|
67
|
-
end
|
|
68
|
-
@tempfile.path
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def unlink_tempfile
|
|
72
|
-
@tempfile&.unlink
|
|
73
51
|
end
|
|
74
52
|
end
|
|
75
53
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lib/aia/handler_context.rb
|
|
4
|
+
#
|
|
5
|
+
# Value object passed to every handler's handle(context) method.
|
|
6
|
+
# All fields are optional (default nil) — each handler reads only what it needs.
|
|
7
|
+
|
|
8
|
+
module AIA
|
|
9
|
+
HandlerContext = Struct.new(
|
|
10
|
+
:robot, # RobotLab::Robot or Network for the current turn
|
|
11
|
+
:prompt, # String — the user prompt
|
|
12
|
+
:config, # AIA config object
|
|
13
|
+
:specialist_type, # String or nil — explicit specialist role for SpawnHandler
|
|
14
|
+
:spawn_spec, # Hash or nil — explicit {name:, model:, provider:, system_prompt:} for SpawnHandler
|
|
15
|
+
keyword_init: true
|
|
16
|
+
)
|
|
17
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lib/aia/handler_protocol.rb
|
|
4
|
+
#
|
|
5
|
+
# Uniform interface for all turn-level handlers.
|
|
6
|
+
# Include this module and implement handle(context) where context
|
|
7
|
+
# is an AIA::HandlerContext value object.
|
|
8
|
+
|
|
9
|
+
module AIA
|
|
10
|
+
module HandlerProtocol
|
|
11
|
+
# Process a turn. Each handler reads only the context fields it needs.
|
|
12
|
+
#
|
|
13
|
+
# @param context [AIA::HandlerContext]
|
|
14
|
+
# @return handler-specific result (String content, Boolean, or nil)
|
|
15
|
+
def handle(context)
|
|
16
|
+
raise NotImplementedError, "#{self.class} must implement #handle(context)"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lib/aia/history_transfer.rb
|
|
4
|
+
#
|
|
5
|
+
# Stateless module for transferring conversation history between robots.
|
|
6
|
+
# Extracted from RobotFactory to isolate the history transfer concern.
|
|
7
|
+
|
|
8
|
+
module AIA
|
|
9
|
+
module HistoryTransfer
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Replay conversation history from an old robot to a new one.
|
|
13
|
+
#
|
|
14
|
+
# Performance: O(N) API calls where N = number of user messages.
|
|
15
|
+
# Each user message triggers a full LLM round-trip on the new model.
|
|
16
|
+
# For a 10-turn conversation with a local model (~1s/turn), expect ~10s.
|
|
17
|
+
# For a cloud model (~2-5s/turn), expect 20-50s. MCP/tools are disabled
|
|
18
|
+
# during replay to avoid side effects.
|
|
19
|
+
def replay_history(old_robot, new_robot)
|
|
20
|
+
return unless old_robot.respond_to?(:messages)
|
|
21
|
+
|
|
22
|
+
old_robot.messages.each do |msg|
|
|
23
|
+
next unless msg.respond_to?(:role) && msg.role == :user
|
|
24
|
+
new_robot.run(msg.content, mcp: :none, tools: :none)
|
|
25
|
+
end
|
|
26
|
+
rescue StandardError => e
|
|
27
|
+
$stderr.puts "Warning: History replay failed: #{e.message}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Summarize conversation history and inject into new robot.
|
|
31
|
+
#
|
|
32
|
+
# Performance: Exactly 2 API calls regardless of conversation length.
|
|
33
|
+
# 1) Summarize on old model (input tokens proportional to conversation)
|
|
34
|
+
# 2) Inject summary into new model (small fixed-size prompt)
|
|
35
|
+
# Faster than :replay for conversations with >2 turns, but loses
|
|
36
|
+
# per-turn context fidelity. Total latency ~4-10s for cloud models.
|
|
37
|
+
def summarize_history(old_robot, new_robot)
|
|
38
|
+
return unless old_robot.respond_to?(:messages) && old_robot.messages.any?
|
|
39
|
+
|
|
40
|
+
summary_lines = old_robot.messages.map do |msg|
|
|
41
|
+
"#{msg.role}: #{msg.content}" if msg.respond_to?(:role)
|
|
42
|
+
end.compact
|
|
43
|
+
|
|
44
|
+
return if summary_lines.empty?
|
|
45
|
+
|
|
46
|
+
summary_prompt = "Summarize this conversation concisely for context transfer:\n#{summary_lines.join("\n")}"
|
|
47
|
+
summary = old_robot.run(summary_prompt, mcp: :none, tools: :none)
|
|
48
|
+
content = summary.respond_to?(:reply) ? summary.reply : summary.to_s
|
|
49
|
+
|
|
50
|
+
new_robot.run("Context from previous conversation: #{content}", mcp: :none, tools: :none)
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
$stderr.puts "Warning: History summarization failed: #{e.message}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/aia/input_collector.rb
CHANGED
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
module AIA
|
|
5
5
|
class InputCollector
|
|
6
|
-
# Collect variable values from user input via
|
|
6
|
+
# Collect variable values from user input via VariableInputCollector
|
|
7
7
|
def collect(parameters)
|
|
8
8
|
return {} if parameters.nil? || parameters.empty?
|
|
9
9
|
|
|
10
10
|
values = {}
|
|
11
|
-
input_manager = AIA::
|
|
11
|
+
input_manager = AIA::VariableInputCollector.new
|
|
12
12
|
|
|
13
13
|
parameters.each do |name, default|
|
|
14
14
|
value = input_manager.request_variable_value(
|
|
15
15
|
variable_name: name,
|
|
16
|
-
default_value: default
|
|
16
|
+
default_value: default
|
|
17
17
|
)
|
|
18
18
|
values[name] = value
|
|
19
19
|
end
|