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.
Files changed (169) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +5 -1
  3. data/.loki +231 -0
  4. data/.quality/flay_baseline.txt +1 -0
  5. data/.quality/flog_baseline.txt +29 -0
  6. data/.quality/reek_baseline.txt +80 -0
  7. data/.rubocop.yml +116 -0
  8. data/.version +1 -1
  9. data/CHANGELOG.md +259 -50
  10. data/IMPLEMENTATION_PLAN.md +506 -0
  11. data/README.md +266 -238
  12. data/Rakefile +118 -5
  13. data/architecture_review.md +314 -0
  14. data/bin/aia +16 -0
  15. data/docs/AGENTS.md +40 -0
  16. data/docs/advanced-prompting.md +67 -3
  17. data/docs/cli-reference.md +312 -56
  18. data/docs/configuration.md +130 -19
  19. data/docs/contributing.md +56 -2
  20. data/docs/directives-reference.md +593 -78
  21. data/docs/faq.md +85 -3
  22. data/docs/guides/available-models.md +1 -1
  23. data/docs/guides/basic-usage.md +6 -6
  24. data/docs/guides/chat.md +40 -16
  25. data/docs/guides/crew.md +239 -0
  26. data/docs/guides/executable-prompts.md +1 -1
  27. data/docs/guides/index.md +1 -0
  28. data/docs/guides/models.md +15 -0
  29. data/docs/index.md +29 -2
  30. data/docs/installation.md +44 -17
  31. data/docs/mcp-integration.md +40 -0
  32. data/docs/prompt_management.md +85 -86
  33. data/docs/security.md +47 -0
  34. data/docs/special_projects_guide.md +386 -0
  35. data/docs/tools-and-mcp-examples.md +23 -0
  36. data/docs/workflows-and-pipelines.md +84 -7
  37. data/examples/.gitignore +1 -0
  38. data/examples/00_setup_aia.sh +27 -44
  39. data/examples/11_multi_model.sh +4 -14
  40. data/examples/12_token_usage.sh +3 -12
  41. data/examples/18_tools.sh +10 -2
  42. data/examples/22_chat_mode.sh +0 -10
  43. data/examples/23_verify.sh +139 -0
  44. data/examples/24_decompose.sh +139 -0
  45. data/examples/25_spawn.sh +139 -0
  46. data/examples/26_debate.sh +97 -0
  47. data/examples/27_mention_routing.sh +157 -0
  48. data/examples/28_model_switching.sh +106 -0
  49. data/examples/29_agent_harness.sh +177 -0
  50. data/examples/README.md +65 -0
  51. data/examples/advanced_multi_robot_capabilities_without_examples.md +106 -0
  52. data/examples/aia_config.yml +1 -1
  53. data/examples/aia_config_orchestrator.yml +45 -0
  54. data/examples/common.sh +18 -6
  55. data/examples/context/tech_stack.md +2 -2
  56. data/examples/prompts_dir/roles/orchestrator.md +21 -0
  57. data/examples/requirements/sinatra_taskflow_app.md +139 -0
  58. data/examples/rules/01_classify_ruby.rb +16 -0
  59. data/examples/rules/02_prefer_claude_for_code.rb +19 -0
  60. data/examples/rules/03_gate_prompt_length.rb +19 -0
  61. data/examples/rules/04_tool_selection.rb +41 -0
  62. data/examples/rules/README.md +30 -0
  63. data/examples/run_all.sh +48 -15
  64. data/examples/tools/word_count_tool.rb +1 -1
  65. data/lib/AGENTS.md +57 -0
  66. data/lib/aia/chat_loop.rb +304 -164
  67. data/lib/aia/config/cli_parser.rb +174 -111
  68. data/lib/aia/config/defaults.yml +62 -33
  69. data/lib/aia/config/mcp_parser.rb +39 -46
  70. data/lib/aia/config/model_spec.rb +34 -2
  71. data/lib/aia/config/validator.rb +108 -142
  72. data/lib/aia/config.rb +110 -145
  73. data/lib/aia/content_extractor.rb +153 -0
  74. data/lib/aia/cost_calculator.rb +38 -0
  75. data/lib/aia/crew.rb +164 -0
  76. data/lib/aia/debate_handler.rb +166 -0
  77. data/lib/aia/delegate_handler.rb +112 -0
  78. data/lib/aia/directive.rb +33 -18
  79. data/lib/aia/directive_processor.rb +16 -7
  80. data/lib/aia/directives/configuration_directives.rb +160 -20
  81. data/lib/aia/directives/context_directives.rb +38 -26
  82. data/lib/aia/directives/execution_directives.rb +136 -4
  83. data/lib/aia/directives/model_directives.rb +76 -34
  84. data/lib/aia/directives/trakflow_directives.rb +44 -0
  85. data/lib/aia/directives/utility_directives.rb +203 -6
  86. data/lib/aia/directives/web_and_file_directives.rb +96 -60
  87. data/lib/aia/errors.rb +15 -0
  88. data/lib/aia/fact_asserter.rb +27 -0
  89. data/lib/aia/fzf.rb +9 -31
  90. data/lib/aia/handler_context.rb +17 -0
  91. data/lib/aia/handler_protocol.rb +19 -0
  92. data/lib/aia/history_transfer.rb +55 -0
  93. data/lib/aia/input_collector.rb +3 -3
  94. data/lib/aia/layered_orchestrator.rb +448 -0
  95. data/lib/aia/logger.rb +24 -4
  96. data/lib/aia/mcp_config_normalizer.rb +35 -0
  97. data/lib/aia/mcp_connection_manager.rb +305 -0
  98. data/lib/aia/mcp_discovery.rb +44 -0
  99. data/lib/aia/mcp_grouper.rb +33 -0
  100. data/lib/aia/mcp_utility.rb +57 -0
  101. data/lib/aia/mention_router.rb +260 -0
  102. data/lib/aia/model_alias_registry.rb +97 -0
  103. data/lib/aia/model_switch_handler.rb +100 -0
  104. data/lib/aia/network_builder.rb +155 -0
  105. data/lib/aia/network_memory_manager.rb +55 -0
  106. data/lib/aia/patches/ruby_llm_streaming_error.rb +43 -0
  107. data/lib/aia/patches/ruby_llm_tool_error.rb +96 -0
  108. data/lib/aia/pipeline_orchestrator.rb +262 -0
  109. data/lib/aia/plugin_loader.rb +170 -0
  110. data/lib/aia/plugin_monitor.rb +208 -0
  111. data/lib/aia/prompt_decomposer.rb +157 -0
  112. data/lib/aia/prompt_handler.rb +19 -39
  113. data/lib/aia/robot_builder.rb +51 -0
  114. data/lib/aia/robot_factory.rb +334 -0
  115. data/lib/aia/robot_namer.rb +116 -0
  116. data/lib/aia/session.rb +83 -17
  117. data/lib/aia/session_tracker.rb +209 -0
  118. data/lib/aia/similarity_scorer.rb +39 -0
  119. data/lib/aia/skill_utils.rb +105 -1
  120. data/lib/aia/spawn_handler.rb +129 -0
  121. data/lib/aia/spawn_spec_parser.rb +65 -0
  122. data/lib/aia/special_mode_handler.rb +302 -0
  123. data/lib/aia/startup_coordinator.rb +150 -0
  124. data/lib/aia/streaming_runner.rb +169 -0
  125. data/lib/aia/system_prompt_assembler.rb +88 -0
  126. data/lib/aia/task_coordinator.rb +202 -0
  127. data/lib/aia/task_decomposer.rb +57 -0
  128. data/lib/aia/task_executor.rb +51 -0
  129. data/lib/aia/tfidf_math.rb +27 -0
  130. data/lib/aia/tool_filter/tfidf.rb +116 -0
  131. data/lib/aia/tool_filter/wordnet_expander.rb +127 -0
  132. data/lib/aia/tool_filter.rb +82 -0
  133. data/lib/aia/tool_filter_registry.rb +30 -0
  134. data/lib/aia/tool_filter_strategy.rb +143 -0
  135. data/lib/aia/tool_loader.rb +210 -0
  136. data/lib/aia/tool_utility.rb +30 -0
  137. data/lib/aia/tools/delegate_to_foreman_tool.rb +70 -0
  138. data/lib/aia/tools/recruit_robot_tool.rb +60 -0
  139. data/lib/aia/tools/reskill_robot_tool.rb +44 -0
  140. data/lib/aia/tools/task_board_tool.rb +114 -0
  141. data/lib/aia/trakflow_bridge.rb +173 -0
  142. data/lib/aia/turn_state.rb +94 -0
  143. data/lib/aia/ui_presenter.rb +166 -198
  144. data/lib/aia/utility.rb +134 -87
  145. data/lib/aia/{history_manager.rb → variable_input_collector.rb} +8 -9
  146. data/lib/aia/verification_network.rb +58 -0
  147. data/lib/aia.rb +108 -63
  148. data/mkdocs.yml +1 -0
  149. metadata +179 -56
  150. data/justfile +0 -215
  151. data/lib/aia/adapter/chat_execution.rb +0 -242
  152. data/lib/aia/adapter/error_handler.rb +0 -68
  153. data/lib/aia/adapter/gem_activator.rb +0 -57
  154. data/lib/aia/adapter/mcp_connector.rb +0 -274
  155. data/lib/aia/adapter/modality_handlers.rb +0 -167
  156. data/lib/aia/adapter/model_registry.rb +0 -81
  157. data/lib/aia/adapter/multi_model_chat.rb +0 -218
  158. data/lib/aia/adapter/provider_configurator.rb +0 -59
  159. data/lib/aia/adapter/tool_filter.rb +0 -85
  160. data/lib/aia/adapter/tool_loader.rb +0 -90
  161. data/lib/aia/chat_processor_service.rb +0 -178
  162. data/lib/aia/prompt_pipeline.rb +0 -183
  163. data/lib/aia/ruby_llm_adapter.rb +0 -95
  164. data/lib/extensions/openstruct_merge.rb +0 -48
  165. data/lib/extensions/ruby_llm/.irbrc +0 -56
  166. data/lib/extensions/ruby_llm/modalities.rb +0 -36
  167. data/lib/extensions/ruby_llm/provider_fix.rb +0 -79
  168. data/lib/refinements/string.rb +0 -16
  169. 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 200 == response.status
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
- alias_method :website, :webpage
34
- alias_method :web, :webpage
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
- .select { |e| Dir.exist?(File.join(dir, e)) }
49
- .sort
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
- skill_data = skill_dirs.filter_map do |name|
57
- fm = parse_front_matter(File.join(dir, name, 'SKILL.md'))
58
- text = "#{name} #{fm.values.join(' ').downcase}"
59
- pos_ok = positive_terms.empty? || positive_terms.all? { |t| text.include?(t) }
60
- neg_ok = negative_terms.none? { |t| text.include?(t) }
61
- [name, fm] if pos_ok && neg_ok
62
- end
63
-
64
- if skill_data.empty?
65
- puts "No skills matching your query"
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
- width = terminal_width
70
- puts "\nAvailable Skills (#{dir}):"
71
- puts "=" * [width, 60].min
72
-
73
- skill_data.each do |name, fm|
74
- display_name = fm['name'] || name
75
- desc_text = fm['description'].to_s.strip
76
- puts "\n #{display_name}"
77
- unless desc_text.empty?
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
- warn "Error: /skill requires a skill name. Use /skills to list available skills."
94
- AIA::LoggerManager.aia_logger.error("/skill requires a skill name")
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
- if path_based_id?(skill_name)
109
- warn "Error: No skill directory found at '#{File.expand_path(skill_name)}'. Use /skills to list available skills."
110
- AIA::LoggerManager.aia_logger.error("No skill directory found at '#{File.expand_path(skill_name)}'")
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
- warn "Error: Skill directory '#{File.basename(skill_dir)}' has no SKILL.md. Use /skills to list available skills."
123
- AIA::LoggerManager.aia_logger.error("Skill directory '#{File.basename(skill_dir)}' has no SKILL.md")
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
- alias_method :clipboard, :paste
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
- list:, # Array of Strings (basenames of files w/o extension)
21
- directory:, # Parent directory of the list items
22
- query: '', # String, the thing be searched for
23
- subject: 'Prompt IDs', # or 'Role Names'
24
- prompt: 'Select one:',
25
- extension: '.md'
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, status = Open3.capture2('fzf', *@fzf_args, stdin_data: input)
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
@@ -3,17 +3,17 @@
3
3
 
4
4
  module AIA
5
5
  class InputCollector
6
- # Collect variable values from user input via HistoryManager
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::HistoryManager.new
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