aia 1.1.1 → 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.
Files changed (175) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +9 -1
  3. data/.loki +11 -0
  4. data/.reek.yml +160 -0
  5. data/.rubocop.yml +116 -0
  6. data/.rubocop_strict.yml +15 -0
  7. data/.version +1 -1
  8. data/CHANGELOG.md +269 -55
  9. data/IMPLEMENTATION_PLAN.md +506 -0
  10. data/README.md +267 -239
  11. data/Rakefile +5 -5
  12. data/_typos.toml +10 -0
  13. data/aia.gemspec +92 -0
  14. data/architecture_review.md +314 -0
  15. data/bin/aia +16 -0
  16. data/config/aia.yml +13 -0
  17. data/docs/AGENTS.md +40 -0
  18. data/docs/advanced-prompting.md +67 -3
  19. data/docs/cli-reference.md +312 -56
  20. data/docs/configuration.md +130 -19
  21. data/docs/contributing.md +56 -2
  22. data/docs/directives-reference.md +593 -78
  23. data/docs/faq.md +85 -3
  24. data/docs/guides/available-models.md +1 -1
  25. data/docs/guides/basic-usage.md +6 -6
  26. data/docs/guides/chat.md +40 -16
  27. data/docs/guides/crew.md +239 -0
  28. data/docs/guides/executable-prompts.md +1 -1
  29. data/docs/guides/index.md +1 -0
  30. data/docs/guides/models.md +15 -0
  31. data/docs/index.md +29 -2
  32. data/docs/installation.md +44 -17
  33. data/docs/mcp-integration.md +40 -0
  34. data/docs/prompt_management.md +85 -86
  35. data/docs/security.md +47 -0
  36. data/docs/special_projects_guide.md +386 -0
  37. data/docs/tools-and-mcp-examples.md +23 -0
  38. data/docs/workflows-and-pipelines.md +84 -7
  39. data/examples/.gitignore +1 -0
  40. data/examples/00_setup_aia.sh +27 -44
  41. data/examples/11_multi_model.sh +4 -14
  42. data/examples/12_token_usage.sh +3 -12
  43. data/examples/18_tools.sh +10 -2
  44. data/examples/22_chat_mode.sh +0 -10
  45. data/examples/23_verify.sh +139 -0
  46. data/examples/24_decompose.sh +139 -0
  47. data/examples/25_spawn.sh +139 -0
  48. data/examples/26_debate.sh +97 -0
  49. data/examples/27_mention_routing.sh +157 -0
  50. data/examples/28_model_switching.sh +106 -0
  51. data/examples/29_agent_harness.sh +177 -0
  52. data/examples/README.md +65 -0
  53. data/examples/advanced_multi_robot_capabilities_without_examples.md +106 -0
  54. data/examples/aia_config.yml +1 -1
  55. data/examples/aia_config_orchestrator.yml +45 -0
  56. data/examples/common.sh +18 -6
  57. data/examples/context/tech_stack.md +2 -2
  58. data/examples/prompts_dir/roles/orchestrator.md +21 -0
  59. data/examples/requirements/sinatra_taskflow_app.md +139 -0
  60. data/examples/rules/01_classify_ruby.rb +16 -0
  61. data/examples/rules/02_prefer_claude_for_code.rb +19 -0
  62. data/examples/rules/03_gate_prompt_length.rb +19 -0
  63. data/examples/rules/04_tool_selection.rb +41 -0
  64. data/examples/rules/README.md +30 -0
  65. data/examples/run_all.sh +48 -15
  66. data/examples/tools/word_count_tool.rb +1 -1
  67. data/lib/AGENTS.md +57 -0
  68. data/lib/aia/chat_loop.rb +263 -167
  69. data/lib/aia/config/cli_parser.rb +217 -145
  70. data/lib/aia/config/defaults.yml +62 -33
  71. data/lib/aia/config/mcp_parser.rb +52 -51
  72. data/lib/aia/config/model_spec.rb +34 -2
  73. data/lib/aia/config/validator.rb +171 -216
  74. data/lib/aia/config.rb +111 -145
  75. data/lib/aia/content_extractor.rb +155 -0
  76. data/lib/aia/cost_calculator.rb +39 -0
  77. data/lib/aia/crew.rb +164 -0
  78. data/lib/aia/debate_handler.rb +174 -0
  79. data/lib/aia/delegate_handler.rb +116 -0
  80. data/lib/aia/directive.rb +43 -26
  81. data/lib/aia/directive_processor.rb +16 -7
  82. data/lib/aia/directives/configuration_directives.rb +214 -60
  83. data/lib/aia/directives/context_directives.rb +67 -52
  84. data/lib/aia/directives/execution_directives.rb +141 -4
  85. data/lib/aia/directives/model_directives.rb +163 -141
  86. data/lib/aia/directives/trakflow_directives.rb +62 -0
  87. data/lib/aia/directives/utility_directives.rb +227 -30
  88. data/lib/aia/directives/web_and_file_directives.rb +126 -77
  89. data/lib/aia/errors.rb +15 -0
  90. data/lib/aia/fact_asserter.rb +27 -0
  91. data/lib/aia/fzf.rb +9 -31
  92. data/lib/aia/handler_context.rb +17 -0
  93. data/lib/aia/handler_protocol.rb +19 -0
  94. data/lib/aia/history_transfer.rb +55 -0
  95. data/lib/aia/input_collector.rb +3 -3
  96. data/lib/aia/layered_orchestrator.rb +471 -0
  97. data/lib/aia/logger.rb +45 -25
  98. data/lib/aia/mcp_config_normalizer.rb +35 -0
  99. data/lib/aia/mcp_connection_manager.rb +315 -0
  100. data/lib/aia/mcp_discovery.rb +44 -0
  101. data/lib/aia/mcp_grouper.rb +33 -0
  102. data/lib/aia/mcp_server_config.rb +30 -0
  103. data/lib/aia/mcp_utility.rb +60 -0
  104. data/lib/aia/mention_router.rb +217 -0
  105. data/lib/aia/model_alias_registry.rb +97 -0
  106. data/lib/aia/model_switch_handler.rb +100 -0
  107. data/lib/aia/network_builder.rb +160 -0
  108. data/lib/aia/network_memory_manager.rb +55 -0
  109. data/lib/aia/patches/ruby_llm_streaming_error.rb +43 -0
  110. data/lib/aia/patches/ruby_llm_tool_error.rb +96 -0
  111. data/lib/aia/pipeline_orchestrator.rb +272 -0
  112. data/lib/aia/plugin_loader.rb +170 -0
  113. data/lib/aia/plugin_monitor.rb +211 -0
  114. data/lib/aia/prompt_decomposer.rb +159 -0
  115. data/lib/aia/prompt_handler.rb +56 -82
  116. data/lib/aia/robot_builder.rb +51 -0
  117. data/lib/aia/robot_factory.rb +338 -0
  118. data/lib/aia/robot_namer.rb +110 -0
  119. data/lib/aia/session.rb +87 -17
  120. data/lib/aia/session_tracker.rb +207 -0
  121. data/lib/aia/similarity_scorer.rb +41 -0
  122. data/lib/aia/skill_utils.rb +105 -1
  123. data/lib/aia/spawn_handler.rb +129 -0
  124. data/lib/aia/spawn_spec_parser.rb +65 -0
  125. data/lib/aia/special_mode_handler.rb +322 -0
  126. data/lib/aia/speech.rb +67 -0
  127. data/lib/aia/startup_coordinator.rb +151 -0
  128. data/lib/aia/streaming_runner.rb +172 -0
  129. data/lib/aia/system_prompt_assembler.rb +92 -0
  130. data/lib/aia/task_coordinator.rb +207 -0
  131. data/lib/aia/task_decomposer.rb +57 -0
  132. data/lib/aia/task_executor.rb +51 -0
  133. data/lib/aia/tfidf_math.rb +27 -0
  134. data/lib/aia/timing.rb +15 -0
  135. data/lib/aia/tool_filter/tfidf.rb +116 -0
  136. data/lib/aia/tool_filter/wordnet_expander.rb +127 -0
  137. data/lib/aia/tool_filter.rb +83 -0
  138. data/lib/aia/tool_filter_registry.rb +30 -0
  139. data/lib/aia/tool_filter_strategy.rb +146 -0
  140. data/lib/aia/tool_introspection.rb +17 -0
  141. data/lib/aia/tool_loader.rb +216 -0
  142. data/lib/aia/tool_utility.rb +30 -0
  143. data/lib/aia/tools/delegate_to_foreman_tool.rb +70 -0
  144. data/lib/aia/tools/recruit_robot_tool.rb +60 -0
  145. data/lib/aia/tools/reskill_robot_tool.rb +44 -0
  146. data/lib/aia/tools/task_board_tool.rb +115 -0
  147. data/lib/aia/trakflow_bridge.rb +175 -0
  148. data/lib/aia/turn_state.rb +95 -0
  149. data/lib/aia/ui_presenter.rb +182 -206
  150. data/lib/aia/utility.rb +136 -87
  151. data/lib/aia/{history_manager.rb → variable_input_collector.rb} +9 -9
  152. data/lib/aia/verification_network.rb +57 -0
  153. data/lib/aia.rb +124 -63
  154. data/mkdocs.yml +1 -0
  155. metadata +187 -58
  156. data/justfile +0 -215
  157. data/lib/aia/adapter/chat_execution.rb +0 -242
  158. data/lib/aia/adapter/error_handler.rb +0 -68
  159. data/lib/aia/adapter/gem_activator.rb +0 -57
  160. data/lib/aia/adapter/mcp_connector.rb +0 -274
  161. data/lib/aia/adapter/modality_handlers.rb +0 -167
  162. data/lib/aia/adapter/model_registry.rb +0 -81
  163. data/lib/aia/adapter/multi_model_chat.rb +0 -218
  164. data/lib/aia/adapter/provider_configurator.rb +0 -59
  165. data/lib/aia/adapter/tool_filter.rb +0 -85
  166. data/lib/aia/adapter/tool_loader.rb +0 -90
  167. data/lib/aia/chat_processor_service.rb +0 -178
  168. data/lib/aia/prompt_pipeline.rb +0 -183
  169. data/lib/aia/ruby_llm_adapter.rb +0 -95
  170. data/lib/extensions/openstruct_merge.rb +0 -48
  171. data/lib/extensions/ruby_llm/.irbrc +0 -56
  172. data/lib/extensions/ruby_llm/modalities.rb +0 -36
  173. data/lib/extensions/ruby_llm/provider_fix.rb +0 -79
  174. data/lib/refinements/string.rb +0 -16
  175. data/main.just +0 -76
@@ -0,0 +1,338 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/aia/robot_factory.rb
4
+ #
5
+ # The core new abstraction in AIA v2.
6
+ # Builds RobotLab::Robot or RobotLab::Network instances from AIA configuration.
7
+ # Replaces the entire lib/aia/adapter/ directory from v1.
8
+
9
+ require 'pm'
10
+ require_relative 'robot_namer'
11
+ require_relative 'tool_loader'
12
+ require_relative 'system_prompt_assembler'
13
+ require_relative 'network_builder'
14
+ require_relative 'history_transfer'
15
+
16
+ module AIA
17
+ # :reek:TooManyMethods -- factory facade: every robot/network build variant plus its run-config helpers lives behind one entry point
18
+ class RobotFactory
19
+ # The max_tokens default from defaults.yml — optimised for cloud APIs that treat
20
+ # it as an output-only cap. Local providers (Ollama, LM Studio) bound input+output
21
+ # to a shared context window, so sending this value causes truncation on long turns.
22
+ DEFAULT_LOCAL_MAX_TOKENS = 32_767
23
+
24
+ class << self
25
+ # One-time startup configuration of RobotLab loggers and providers.
26
+ # Must be called once before the first build (e.g., in AIA.run).
27
+ #
28
+ # @param config [AIA::Config] the AIA configuration
29
+ def setup(config)
30
+ configure_robot_lab(config)
31
+ end
32
+
33
+ # Build a Robot or Network from the current AIA config
34
+ #
35
+ # @param config [AIA::Config] the AIA configuration
36
+ # @return [RobotLab::Robot, RobotLab::Network] the built robot or network
37
+ def build(config = AIA.config)
38
+ namer = RobotNamer.new(first_name: 'Tobor')
39
+ apply_tool_cache(config)
40
+
41
+ # The interactive session always runs a *crew* (a Network), so robots can
42
+ # be recruited/dropped at runtime. A single model is wrapped in a one-member
43
+ # crew whose chief is that robot; multi-model aggregation networks are
44
+ # already Networks. (Normal turns still stream via the chief — see ChatLoop.)
45
+ if config.models.length > 1
46
+ build_multi_model(config, namer)
47
+ else
48
+ build_crew(build_single_robot(config, namer))
49
+ end
50
+ rescue RubyLLM::ModelNotFoundError => e
51
+ model_names = config.models.map(&:name).join(', ')
52
+ raise AIA::ConfigurationError,
53
+ "#{e.message}\nRequested model(s): #{model_names}\nRun 'aia --available-models' to see available models."
54
+ end
55
+
56
+ # Rebuild robot(s) after config changes (e.g., /model, /config directives).
57
+ # Supports conversation history transfer modes.
58
+ # Reuses cached tools from the initial build (I4/I5).
59
+ #
60
+ # @param config [AIA::Config] the AIA configuration
61
+ # @param history_mode [Symbol] :clean, :replay, or :summarize
62
+ # @return [RobotLab::Robot, RobotLab::Network]
63
+ def rebuild(config = AIA.config, history_mode: :clean)
64
+ old_robot = AIA.client
65
+ new_robot = build(config)
66
+
67
+ case history_mode
68
+ when :replay
69
+ HistoryTransfer.replay_history(old_robot, new_robot)
70
+ when :summarize
71
+ HistoryTransfer.summarize_history(old_robot, new_robot)
72
+ end
73
+
74
+ new_robot
75
+ end
76
+
77
+ # --- Forwarding wrappers for backward compatibility ---
78
+
79
+ def clear_tool_cache!
80
+ ToolLoader.clear_cache!
81
+ end
82
+
83
+ def filtered_tools(config)
84
+ ToolLoader.filtered_tools(config)
85
+ end
86
+
87
+ def resolve_system_prompt(config, model_spec = nil)
88
+ SystemPromptAssembler.resolve_system_prompt(config, model_spec)
89
+ end
90
+
91
+ def build_identity_prompt(robot_name, spec, roster)
92
+ SystemPromptAssembler.build_identity_prompt(robot_name, spec, roster)
93
+ end
94
+
95
+ # Build a concurrent MCP network where independent server groups
96
+ # run in parallel with a synthesizer to merge results.
97
+ #
98
+ # @param config [AIA::Config] the AIA configuration
99
+ # @param server_groups [Array<Array<Hash>>] groups of MCP server configs
100
+ # @return [RobotLab::Network]
101
+ def build_concurrent_mcp_network(config, server_groups)
102
+ namer = RobotNamer.new(first_name: 'Tobor')
103
+ NetworkBuilder.build_concurrent_mcp_network(config, namer, server_groups)
104
+ end
105
+
106
+ # Build a single robot for one model.
107
+ # Delegates to RobotBuilder for single-robot construction.
108
+ #
109
+ # @param config [AIA::Config] the AIA configuration
110
+ # @param namer [AIA::RobotNamer] fresh namer for this build
111
+ # @return [RobotLab::Robot]
112
+ def build_single_robot(config, namer)
113
+ RobotBuilder.build(config, namer: namer)
114
+ end
115
+
116
+ # Reuse the build-time tool cache when present, else load tools fresh.
117
+ def apply_tool_cache(config)
118
+ cached = ToolLoader.cached_tools
119
+ if cached
120
+ config.loaded_tools = cached
121
+ config.tool_names = cached.map { |t| t.respond_to?(:name) ? t.name : t.class.name }.join(', ')
122
+ else
123
+ ToolLoader.load_tools(config)
124
+ end
125
+ end
126
+
127
+ # Wrap a single robot in a one-member crew Network so the session can
128
+ # recruit/drop robots at runtime. The robot becomes the crew's chief
129
+ # (the lone pipeline task); normal turns are routed to it directly so
130
+ # token streaming is preserved.
131
+ #
132
+ # @param chief [RobotLab::Robot]
133
+ # @return [RobotLab::Network]
134
+ def build_crew(chief)
135
+ RobotLab.create_network(name: 'aia-crew') do
136
+ task chief.name.to_sym, chief, depends_on: :none
137
+ end
138
+ end
139
+
140
+ # Build RunConfig from AIA configuration.
141
+ # Uses the ruby_llm model registry to skip parameters the model doesn't support:
142
+ # - temperature is omitted for models that declare temperature: false in metadata
143
+ # - max_tokens remains a RobotLab::RunConfig field; AIA normalizes it at
144
+ # the RubyLLM/OpenAI provider boundary for models that need
145
+ # max_completion_tokens
146
+ # :reek:TooManyStatements -- one guarded param per LLM tuning knob, with provider-specific max_tokens handling
147
+ def build_run_config(config)
148
+ model_spec = config.models.first
149
+ llm = config.llm
150
+ params = {}
151
+
152
+ temp = llm.temperature
153
+ params[:temperature] = temp if temp && model_supports_temperature?(model_spec)
154
+
155
+ max = llm.max_tokens
156
+ # Local providers (Ollama, LM Studio) have a small total context window
157
+ # (input + output combined, typically 32768 tokens for qwen3-class models).
158
+ # Sending the cloud default of 32767 as num_predict leaves almost no room
159
+ # once the input context grows across turns, causing mid-response truncation.
160
+ # Skip max_tokens for local providers when it's still at the cloud default;
161
+ # honour it when the user has explicitly set a smaller value.
162
+ local = model_spec&.local_provider?
163
+ params[:max_tokens] = max if max && !(local && max == DEFAULT_LOCAL_MAX_TOKENS)
164
+
165
+ tp = llm.top_p
166
+ params[:top_p] = tp if tp
167
+
168
+ fp = llm.frequency_penalty
169
+ params[:frequency_penalty] = fp if fp && fp != 0.0
170
+
171
+ pp = llm.presence_penalty
172
+ params[:presence_penalty] = pp if pp && pp != 0.0
173
+
174
+ RobotLab::RunConfig.new(**params)
175
+ end
176
+
177
+ # Returns false for models that explicitly declare temperature: false in the
178
+ # ruby_llm model registry (e.g. gpt-5.4, o1, o3 reasoning models).
179
+ def model_supports_temperature?(model_spec)
180
+ return true unless model_spec
181
+ info = RubyLLM.models.find(model_spec.name)
182
+ info.metadata[:temperature] != false
183
+ rescue StandardError
184
+ true
185
+ end
186
+
187
+ # Normalize all configured MCP server configs for robot_lab.
188
+ # Filtering/selection is MCPDiscovery's responsibility — this just normalizes shape.
189
+ def mcp_server_configs(config)
190
+ return [] if config.flags.no_mcp
191
+ Array(config.mcp_servers).map { |s| MCPConfigNormalizer.normalize(s) }
192
+ end
193
+
194
+ # Normalize a single MCP server config to robot_lab's nested transport format.
195
+ # Delegates to MCPConfigNormalizer.
196
+ def normalize_mcp_config(server)
197
+ MCPConfigNormalizer.normalize(server)
198
+ end
199
+
200
+ # Initialize shared memory for a network with session context.
201
+ # Delegates to NetworkMemoryManager.
202
+ #
203
+ # @param network [RobotLab::Network]
204
+ # @param config [AIA::Config]
205
+ # @return [RobotLab::Network]
206
+ def initialize_network_memory(network, config)
207
+ NetworkMemoryManager.initialize_memory(network, config)
208
+ end
209
+
210
+ # Set up memory subscriptions for debug logging and completion tracking.
211
+ # Delegates to NetworkMemoryManager.
212
+ #
213
+ # @param network [RobotLab::Network]
214
+ # @param config [AIA::Config]
215
+ def setup_memory_subscriptions(network, config)
216
+ NetworkMemoryManager.setup_subscriptions(network, config)
217
+ end
218
+
219
+ # Attach a shared TypedBus message bus to all robots in a network.
220
+ #
221
+ # @param network [RobotLab::Network]
222
+ # @return [TypedBus::MessageBus, nil]
223
+ def attach_bus(network)
224
+ return nil unless network.respond_to?(:robots)
225
+
226
+ bus = TypedBus::MessageBus.new
227
+ network.robots.each_value { |robot| robot.with_bus(bus) }
228
+ bus
229
+ end
230
+
231
+ # Build a RobotLab::Robot for a model spec, applying provider resolution.
232
+ # Centralizes the "set provider if the spec has one, then build" tail that
233
+ # every robot/network builder needs, so local-provider routing (ollama/lms)
234
+ # can't be forgotten at a new build site.
235
+ #
236
+ # @param spec [AIA::Config::ModelSpec] the model spec (supplies model: and provider)
237
+ # @param opts [Hash] forwarded to RobotLab.build (name:, system_prompt:,
238
+ # local_tools:, mcp_servers:, config:, on_content:, ...). Do NOT pass model:.
239
+ # @return [RobotLab::Robot]
240
+ def build_robot(spec, **opts)
241
+ opts[:provider] = resolve_provider(spec) if spec.provider
242
+ RobotLab.build(model: spec.name, **opts)
243
+ end
244
+
245
+ # Map AIA provider aliases to RubyLLM provider slugs.
246
+ def resolve_provider(model_spec)
247
+ case model_spec.provider
248
+ when 'lms' then 'openai'
249
+ else model_spec.provider
250
+ end
251
+ end
252
+
253
+ private
254
+
255
+ # Configure RobotLab providers from AIA config
256
+ def configure_robot_lab(config)
257
+ # Configure loggers before any API calls
258
+ AIA::LoggerManager.configure_llm_logger
259
+ AIA::LoggerManager.configure_mcp_logger
260
+
261
+ # Route RobotLab's internal logging through AIA's MCP logger
262
+ # so entries go to the configured log files instead of STDOUT.
263
+ # Must use direct assignment — block form doesn't set runtime attrs.
264
+ RobotLab.config.logger = AIA::LoggerManager.mcp_logger
265
+
266
+ RobotLab.config do |c|
267
+ c.ruby_llm do |r|
268
+ r.request_timeout = 120
269
+ end
270
+ end
271
+
272
+ # RobotLab's after_load sets PM.config.prompts_dir to its own default ('prompts').
273
+ # Restore AIA's configured prompts dir so PM.parse finds the correct files.
274
+ PM.configure { |c| c.prompts_dir = AIA.config.prompts.dir }
275
+
276
+ # Configure local provider API endpoints from environment variables
277
+ configure_local_providers(config)
278
+
279
+ # Wire audio model settings into RubyLLM config
280
+ configure_audio(config)
281
+ end
282
+
283
+ # Wire AIA's audio config into RubyLLM so transcription calls pick up
284
+ # the user-configured model without requiring explicit model: arguments.
285
+ # speech_model has no ruby_llm TTS API backing yet — it is exposed as
286
+ # the SPEECH_MODEL env var for custom speak_command scripts.
287
+ def configure_audio(config)
288
+ transcription_model = config.audio&.transcription_model
289
+ return unless transcription_model
290
+
291
+ RubyLLM.configure { |c| c.default_transcription_model = transcription_model }
292
+ end
293
+
294
+ # Set API base URLs for local providers (Ollama, LM Studio) so
295
+ # RubyLLM can reach them. Reads from standard env vars.
296
+ def configure_local_providers(config)
297
+ providers_used = config.models.map(&:provider).compact.uniq
298
+
299
+ RubyLLM.configure do |c|
300
+ # Always configure Ollama's base so switching to an `ollama/` model
301
+ # mid-session works even when the session started on a cloud model.
302
+ # It only affects the Ollama provider, so it's harmless when unused.
303
+ # RubyLLM's Ollama provider subclasses OpenAI and appends
304
+ # `chat/completions`, so the base must include the `/v1` suffix.
305
+ c.ollama_api_base = ENV.fetch('OLLAMA_API_BASE', 'http://localhost:11434/v1')
306
+
307
+ # LM Studio exposes an OpenAI-compatible API and reuses the OpenAI
308
+ # provider, so only override the OpenAI base when an `lms/` model is
309
+ # actually in use — otherwise it would hijack real OpenAI requests.
310
+ # The OpenAI provider POSTs the relative `chat/completions`, so (like
311
+ # the real `https://api.openai.com/v1` default and the Ollama base) the
312
+ # LM Studio base must include the `/v1` suffix — otherwise requests hit
313
+ # `:1234/chat/completions`, LM Studio 404s, and ruby_llm's streaming
314
+ # error handler masks it as `delete_prefix for nil`.
315
+ if providers_used.include?('lms')
316
+ c.openai_api_base = ENV.fetch('LMS_API_BASE', 'http://localhost:1234/v1')
317
+ end
318
+ end
319
+ end
320
+
321
+ # Decide between consensus and parallel multi-model.
322
+ # Initializes shared memory and subscriptions on the resulting network.
323
+ #
324
+ # @param config [AIA::Config] the AIA configuration
325
+ # @param namer [AIA::RobotNamer] fresh namer for this build
326
+ def build_multi_model(config, namer)
327
+ network = if config.flags.consensus
328
+ NetworkBuilder.build_consensus_network(config, namer)
329
+ else
330
+ NetworkBuilder.build_parallel_network(config, namer)
331
+ end
332
+ initialize_network_memory(network, config)
333
+ setup_memory_subscriptions(network, config)
334
+ network
335
+ end
336
+ end
337
+ end
338
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/aia/robot_namer.rb
4
+ #
5
+ # Generates memorable one-word names for robots based on their model.
6
+ # Used instead of technical names like "aia-claude-sonnet-4".
7
+
8
+ module AIA
9
+ class RobotNamer
10
+ # Curated names for known model patterns (matched in order, first wins)
11
+ MODEL_NAMES = [
12
+ # Claude family
13
+ [/claude.*opus.*4/i, 'Virtuoso'],
14
+ [/claude.*opus/i, 'Maestro'],
15
+ [/claude.*sonnet.*4/i, 'Lyric'],
16
+ [/claude.*sonnet.*3\.5/i, 'Muse'],
17
+ [/claude.*sonnet/i, 'Poet'],
18
+ [/claude.*haiku/i, 'Zen'],
19
+
20
+ # GPT family
21
+ [/gpt-4\.1/i, 'Vanguard'],
22
+ [/gpt-4o-mini/i, 'Spark'],
23
+ [/gpt-4o/i, 'Flash'],
24
+ [/gpt-4-turbo/i, 'Bolt'],
25
+ [/gpt-4/i, 'Titan'],
26
+ [/gpt-3.*turbo/i, 'Dash'],
27
+ [/gpt-5-nano/i, 'Mote'],
28
+ [/gpt-5-mini/i, 'Flare'],
29
+ [/gpt-5/i, 'Apex'],
30
+ [/o1-mini/i, 'Ponder'],
31
+ [/o1-pro/i, 'Scholar'],
32
+ [/o1\b/i, 'Thinker'],
33
+ [/o3-mini/i, 'Deduce'],
34
+ [/o3\b/i, 'Reason'],
35
+ [/o4-mini/i, 'Nimble'],
36
+
37
+ # Google
38
+ [/gemini.*flash/i, 'Prism'],
39
+ [/gemini.*pro/i, 'Atlas'],
40
+ [/gemini/i, 'Twin'],
41
+
42
+ # Open source / local
43
+ [/llama/i, 'Sherpa'],
44
+ [/mistral.*large/i, 'Cyclone'],
45
+ [/mistral/i, 'Gale'],
46
+ [/mixtral/i, 'Tempest'],
47
+ [/phi/i, 'Quark'],
48
+ [/qwen/i, 'Jade'],
49
+ [/deepseek.*coder/i, 'Cipher'],
50
+ [/deepseek/i, 'Diver'],
51
+ [/codestral/i, 'Rune'],
52
+ [/command.*r\+/i, 'Admiral'],
53
+ [/command/i, 'Captain'],
54
+ [/falcon/i, 'Talon'],
55
+ [/vicuna/i, 'Voyager'],
56
+ [/yi\b/i, 'Sage'],
57
+ [/starcoder/i, 'Nova'],
58
+ [/granite/i, 'Monolith'],
59
+ [/solar/i, 'Corona'],
60
+ [/orca/i, 'Tide'],
61
+ [/wizard/i, 'Merlin'],
62
+ [/dolphin/i, 'Reef'],
63
+ [/nous/i, 'Oracle'],
64
+ [/stable/i, 'Anchor'],
65
+ [/gemma/i, 'Jewel']
66
+ ].freeze
67
+
68
+ # Fallback pool for unrecognized models — deterministically
69
+ # selected by hashing the model name
70
+ FALLBACK_NAMES = %w[
71
+ Beacon Drift Echo Fable Glimmer Halo Iris
72
+ Kindle Lumen Mirth Nexus Orbit Pixel Quill
73
+ Ripple Sentry Trace Unity Vault Wisp Zephyr
74
+ Ember Frost Crest Flint Haze Slate Plume
75
+ Comet Aura Blaze Dusk Flux Helix Onyx
76
+ ].freeze
77
+
78
+ def initialize(first_name: nil)
79
+ @first_name = first_name
80
+ @used_names = {}
81
+ end
82
+
83
+ def name_for(model_name)
84
+ if @used_names.empty? && @first_name
85
+ return unique_name(@first_name)
86
+ end
87
+
88
+ model_name = model_name.to_s
89
+ base = find_base_name(model_name)
90
+ unique_name(base)
91
+ end
92
+
93
+ private
94
+
95
+ def find_base_name(model_name)
96
+ MODEL_NAMES.each do |pattern, name|
97
+ return name if model_name.match?(pattern)
98
+ end
99
+
100
+ # Deterministic fallback based on model name
101
+ index = model_name.bytes.sum % FALLBACK_NAMES.size
102
+ FALLBACK_NAMES[index]
103
+ end
104
+
105
+ def unique_name(base)
106
+ count = @used_names[base] = (@used_names[base] || 0) + 1
107
+ count == 1 ? base : "#{base}#{count}"
108
+ end
109
+ end
110
+ end
data/lib/aia/session.rb CHANGED
@@ -1,7 +1,11 @@
1
- # lib/aia/session.rb
2
1
  # frozen_string_literal: true
3
2
 
4
- require "tty-spinner"
3
+ # lib/aia/session.rb
4
+ #
5
+ # Simplified orchestrator for AIA v2.
6
+ # Builds robot via RobotFactory, processes pipeline, enters chat.
7
+ # Integrates TrakFlow for pipeline tracking and session continuity.
8
+
5
9
  require "tty-screen"
6
10
  require "reline"
7
11
  require "pm"
@@ -9,17 +13,20 @@ require "json"
9
13
  require "fileutils"
10
14
  require "amazing_print"
11
15
  require_relative "directive_processor"
12
- require_relative "history_manager"
16
+ require_relative "variable_input_collector"
13
17
  require_relative "ui_presenter"
14
- require_relative "chat_processor_service"
15
18
  require_relative "prompt_handler"
16
19
  require_relative "utility"
17
20
  require_relative "input_collector"
18
- require_relative "prompt_pipeline"
21
+ require_relative "robot_factory"
22
+ require_relative "tool_filter_registry"
19
23
  require_relative "chat_loop"
20
24
 
21
25
  module AIA
26
+ # :reek:TooManyInstanceVariables -- session hub wires presenter, trackers, handlers, and robot; each collaborator is a named field
22
27
  class Session
28
+ include ContentExtractor
29
+
23
30
  def initialize(prompt_handler)
24
31
  @prompt_handler = prompt_handler
25
32
 
@@ -29,7 +36,23 @@ module AIA
29
36
 
30
37
  # Starts the session, processing all prompts in the pipeline and then
31
38
  # optionally starting an interactive chat session.
39
+ # :reek:TooManyStatements -- session boot script: build robot, startup coordination, optional immediate chat, pipeline run, chat handoff
32
40
  def start
41
+ cfg = AIA.config
42
+
43
+ # Build robot or network
44
+ @robot = RobotFactory.build(cfg)
45
+ AIA.client = @robot
46
+
47
+ # Run all startup coordination: MCP, tools, filters, task board, bus
48
+ coordinator = StartupCoordinator.new(robot: @robot, ui_presenter: @ui_presenter)
49
+ coordinator.run(cfg)
50
+ @filters = coordinator.filters
51
+ @mcp_manager = coordinator.mcp_manager
52
+
53
+ # Store session tracker globally for KBS access
54
+ AIA.session_tracker = @session_tracker
55
+
33
56
  # Handle special chat-only cases first
34
57
  if should_start_chat_immediately?
35
58
  AIA::Utility.robot
@@ -38,38 +61,85 @@ module AIA
38
61
  end
39
62
 
40
63
  # Process all prompts in the pipeline
41
- @prompt_pipeline.process_all
64
+ PipelineOrchestrator.new(
65
+ robot: @robot,
66
+ prompt_handler: @prompt_handler,
67
+ input_collector: @input_collector,
68
+ ui_presenter: @ui_presenter,
69
+ session_tracker: @session_tracker
70
+ ).process(cfg)
42
71
 
43
72
  # Start chat mode after all prompts are processed
44
- if AIA.chat?
45
- AIA::Utility.robot
46
- @ui_presenter.display_separator
47
- @chat_loop.start(skip_context_files: true)
48
- end
73
+ return unless AIA.chat?
74
+ @chat_loop = build_chat_loop
75
+ AIA::Utility.robot
76
+ @ui_presenter.display_separator
77
+ @chat_loop.start(skip_context_files: true)
78
+ end
79
+
80
+ # Release all held resources: MCP connections and filter state.
81
+ # Called automatically via at_exit registered in #start.
82
+ # Safe to call multiple times.
83
+ def cleanup
84
+ @mcp_manager&.close_all
85
+ @filters&.each_value(&:cleanup)
49
86
  end
50
87
 
88
+ MAX_HISTORY_BYTES = 10 * 1024 * 1024 # 10 MB
89
+
51
90
  private
52
91
 
53
92
  def initialize_components
54
93
  @ui_presenter = UIPresenter.new
55
94
  @directive_processor = DirectiveProcessor.new
56
- @chat_processor = ChatProcessorService.new(@ui_presenter, @directive_processor)
57
95
  @input_collector = InputCollector.new
58
- @prompt_pipeline = PromptPipeline.new(@prompt_handler, @chat_processor, @ui_presenter, @input_collector)
59
- @chat_loop = ChatLoop.new(@chat_processor, @ui_presenter, @directive_processor)
96
+
97
+ @session_tracker = SessionTracker.new
98
+ @alias_registry = ModelAliasRegistry.new(
99
+ AIA.config.respond_to?(:model_aliases) ? (AIA.config.model_aliases || {}) : {}
100
+ )
101
+ @chat_loop = nil # created after robot is built
102
+
103
+ rotate_history_log_if_needed
104
+ end
105
+
106
+ # Rotate the prompt history log if it has grown too large.
107
+ # Renames <file> to <file>.1 so the next session starts fresh.
108
+ # Called once at session initialization.
109
+ def rotate_history_log_if_needed
110
+ history_file = AIA.config.output.history_file
111
+ return unless history_file && File.exist?(history_file)
112
+ return unless File.size(history_file) > MAX_HISTORY_BYTES
113
+
114
+ archive = "#{history_file}.1"
115
+ FileUtils.mv(history_file, archive)
116
+ $stderr.puts "History log rotated: #{File.basename(history_file)} → #{File.basename(archive)}"
117
+ rescue StandardError => e
118
+ $stderr.puts "Warning: Could not rotate history log: #{e.message}"
60
119
  end
61
120
 
62
121
  def setup_output_file
63
122
  out_file = AIA.config.output.file
64
- if out_file && !out_file.nil? && !AIA.append? && File.exist?(out_file)
65
- File.open(out_file, "w") { } # Truncate the file
66
- end
123
+ return unless out_file && !out_file.nil? && !AIA.append? && File.exist?(out_file)
124
+ File.write(out_file, '')
125
+ end
126
+
127
+ def build_chat_loop
128
+ ChatLoop.new(
129
+ @robot, @ui_presenter, @directive_processor,
130
+ session_tracker: @session_tracker,
131
+ alias_registry: @alias_registry,
132
+ filters: @filters
133
+ )
67
134
  end
68
135
 
69
136
  # Check if we should start chat immediately without processing any prompts
70
137
  def should_start_chat_immediately?
71
138
  return false unless AIA.chat?
72
139
 
140
+ # Create chat loop now that robot is available
141
+ @chat_loop = build_chat_loop
142
+
73
143
  AIA.config.pipeline.empty? || AIA.config.pipeline.all? { |id| id.nil? || id.empty? }
74
144
  end
75
145
  end