aia 1.1.0 → 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 (170) 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 +266 -42
  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 +19 -0
  55. data/examples/context/tech_stack.md +2 -2
  56. data/examples/prompts_dir/project_summary +2 -2
  57. data/examples/prompts_dir/roles/orchestrator.md +21 -0
  58. data/examples/requirements/sinatra_taskflow_app.md +139 -0
  59. data/examples/rules/01_classify_ruby.rb +16 -0
  60. data/examples/rules/02_prefer_claude_for_code.rb +19 -0
  61. data/examples/rules/03_gate_prompt_length.rb +19 -0
  62. data/examples/rules/04_tool_selection.rb +41 -0
  63. data/examples/rules/README.md +30 -0
  64. data/examples/run_all.sh +48 -15
  65. data/examples/tools/word_count_tool.rb +1 -1
  66. data/lib/AGENTS.md +57 -0
  67. data/lib/aia/chat_loop.rb +306 -159
  68. data/lib/aia/config/cli_parser.rb +174 -111
  69. data/lib/aia/config/defaults.yml +62 -33
  70. data/lib/aia/config/mcp_parser.rb +39 -46
  71. data/lib/aia/config/model_spec.rb +34 -2
  72. data/lib/aia/config/validator.rb +121 -138
  73. data/lib/aia/config.rb +110 -145
  74. data/lib/aia/content_extractor.rb +153 -0
  75. data/lib/aia/cost_calculator.rb +38 -0
  76. data/lib/aia/crew.rb +164 -0
  77. data/lib/aia/debate_handler.rb +166 -0
  78. data/lib/aia/delegate_handler.rb +112 -0
  79. data/lib/aia/directive.rb +33 -18
  80. data/lib/aia/directive_processor.rb +16 -7
  81. data/lib/aia/directives/configuration_directives.rb +160 -20
  82. data/lib/aia/directives/context_directives.rb +38 -26
  83. data/lib/aia/directives/execution_directives.rb +136 -4
  84. data/lib/aia/directives/model_directives.rb +76 -34
  85. data/lib/aia/directives/trakflow_directives.rb +44 -0
  86. data/lib/aia/directives/utility_directives.rb +203 -6
  87. data/lib/aia/directives/web_and_file_directives.rb +96 -60
  88. data/lib/aia/errors.rb +15 -0
  89. data/lib/aia/fact_asserter.rb +27 -0
  90. data/lib/aia/fzf.rb +9 -31
  91. data/lib/aia/handler_context.rb +17 -0
  92. data/lib/aia/handler_protocol.rb +19 -0
  93. data/lib/aia/history_transfer.rb +55 -0
  94. data/lib/aia/input_collector.rb +3 -3
  95. data/lib/aia/layered_orchestrator.rb +448 -0
  96. data/lib/aia/logger.rb +24 -4
  97. data/lib/aia/mcp_config_normalizer.rb +35 -0
  98. data/lib/aia/mcp_connection_manager.rb +305 -0
  99. data/lib/aia/mcp_discovery.rb +44 -0
  100. data/lib/aia/mcp_grouper.rb +33 -0
  101. data/lib/aia/mcp_utility.rb +57 -0
  102. data/lib/aia/mention_router.rb +260 -0
  103. data/lib/aia/model_alias_registry.rb +97 -0
  104. data/lib/aia/model_switch_handler.rb +100 -0
  105. data/lib/aia/network_builder.rb +155 -0
  106. data/lib/aia/network_memory_manager.rb +55 -0
  107. data/lib/aia/patches/ruby_llm_streaming_error.rb +43 -0
  108. data/lib/aia/patches/ruby_llm_tool_error.rb +96 -0
  109. data/lib/aia/pipeline_orchestrator.rb +262 -0
  110. data/lib/aia/plugin_loader.rb +170 -0
  111. data/lib/aia/plugin_monitor.rb +208 -0
  112. data/lib/aia/prompt_decomposer.rb +157 -0
  113. data/lib/aia/prompt_handler.rb +19 -39
  114. data/lib/aia/robot_builder.rb +51 -0
  115. data/lib/aia/robot_factory.rb +334 -0
  116. data/lib/aia/robot_namer.rb +116 -0
  117. data/lib/aia/session.rb +83 -17
  118. data/lib/aia/session_tracker.rb +209 -0
  119. data/lib/aia/similarity_scorer.rb +39 -0
  120. data/lib/aia/skill_utils.rb +105 -1
  121. data/lib/aia/spawn_handler.rb +129 -0
  122. data/lib/aia/spawn_spec_parser.rb +65 -0
  123. data/lib/aia/special_mode_handler.rb +302 -0
  124. data/lib/aia/startup_coordinator.rb +150 -0
  125. data/lib/aia/streaming_runner.rb +169 -0
  126. data/lib/aia/system_prompt_assembler.rb +88 -0
  127. data/lib/aia/task_coordinator.rb +202 -0
  128. data/lib/aia/task_decomposer.rb +57 -0
  129. data/lib/aia/task_executor.rb +51 -0
  130. data/lib/aia/tfidf_math.rb +27 -0
  131. data/lib/aia/tool_filter/tfidf.rb +116 -0
  132. data/lib/aia/tool_filter/wordnet_expander.rb +127 -0
  133. data/lib/aia/tool_filter.rb +82 -0
  134. data/lib/aia/tool_filter_registry.rb +30 -0
  135. data/lib/aia/tool_filter_strategy.rb +143 -0
  136. data/lib/aia/tool_loader.rb +210 -0
  137. data/lib/aia/tool_utility.rb +30 -0
  138. data/lib/aia/tools/delegate_to_foreman_tool.rb +70 -0
  139. data/lib/aia/tools/recruit_robot_tool.rb +60 -0
  140. data/lib/aia/tools/reskill_robot_tool.rb +44 -0
  141. data/lib/aia/tools/task_board_tool.rb +114 -0
  142. data/lib/aia/trakflow_bridge.rb +173 -0
  143. data/lib/aia/turn_state.rb +94 -0
  144. data/lib/aia/ui_presenter.rb +166 -198
  145. data/lib/aia/utility.rb +134 -87
  146. data/lib/aia/{history_manager.rb → variable_input_collector.rb} +8 -9
  147. data/lib/aia/verification_network.rb +58 -0
  148. data/lib/aia.rb +108 -63
  149. data/mkdocs.yml +1 -0
  150. metadata +179 -56
  151. data/justfile +0 -215
  152. data/lib/aia/adapter/chat_execution.rb +0 -242
  153. data/lib/aia/adapter/error_handler.rb +0 -68
  154. data/lib/aia/adapter/gem_activator.rb +0 -57
  155. data/lib/aia/adapter/mcp_connector.rb +0 -274
  156. data/lib/aia/adapter/modality_handlers.rb +0 -167
  157. data/lib/aia/adapter/model_registry.rb +0 -81
  158. data/lib/aia/adapter/multi_model_chat.rb +0 -218
  159. data/lib/aia/adapter/provider_configurator.rb +0 -59
  160. data/lib/aia/adapter/tool_filter.rb +0 -85
  161. data/lib/aia/adapter/tool_loader.rb +0 -90
  162. data/lib/aia/chat_processor_service.rb +0 -164
  163. data/lib/aia/prompt_pipeline.rb +0 -183
  164. data/lib/aia/ruby_llm_adapter.rb +0 -95
  165. data/lib/extensions/openstruct_merge.rb +0 -48
  166. data/lib/extensions/ruby_llm/.irbrc +0 -56
  167. data/lib/extensions/ruby_llm/modalities.rb +0 -36
  168. data/lib/extensions/ruby_llm/provider_fix.rb +0 -79
  169. data/lib/refinements/string.rb +0 -16
  170. data/main.just +0 -76
@@ -0,0 +1,334 @@
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
+ class RobotFactory
18
+ # The max_tokens default from defaults.yml — optimised for cloud APIs that treat
19
+ # it as an output-only cap. Local providers (Ollama, LM Studio) bound input+output
20
+ # to a shared context window, so sending this value causes truncation on long turns.
21
+ DEFAULT_LOCAL_MAX_TOKENS = 32_767
22
+
23
+ class << self
24
+ # One-time startup configuration of RobotLab loggers and providers.
25
+ # Must be called once before the first build (e.g., in AIA.run).
26
+ #
27
+ # @param config [AIA::Config] the AIA configuration
28
+ def setup(config)
29
+ configure_robot_lab(config)
30
+ end
31
+
32
+ # Build a Robot or Network from the current AIA config
33
+ #
34
+ # @param config [AIA::Config] the AIA configuration
35
+ # @return [RobotLab::Robot, RobotLab::Network] the built robot or network
36
+ def build(config = AIA.config)
37
+ namer = RobotNamer.new(first_name: 'Tobor')
38
+ apply_tool_cache(config)
39
+
40
+ # The interactive session always runs a *crew* (a Network), so robots can
41
+ # be recruited/dropped at runtime. A single model is wrapped in a one-member
42
+ # crew whose chief is that robot; multi-model aggregation networks are
43
+ # already Networks. (Normal turns still stream via the chief — see ChatLoop.)
44
+ if config.models.length > 1
45
+ build_multi_model(config, namer)
46
+ else
47
+ build_crew(build_single_robot(config, namer))
48
+ end
49
+ rescue RubyLLM::ModelNotFoundError => e
50
+ model_names = config.models.map(&:name).join(', ')
51
+ raise AIA::ConfigurationError,
52
+ "#{e.message}\nRequested model(s): #{model_names}\nRun 'aia --available-models' to see available models."
53
+ end
54
+
55
+ # Rebuild robot(s) after config changes (e.g., /model, /config directives).
56
+ # Supports conversation history transfer modes.
57
+ # Reuses cached tools from the initial build (I4/I5).
58
+ #
59
+ # @param config [AIA::Config] the AIA configuration
60
+ # @param history_mode [Symbol] :clean, :replay, or :summarize
61
+ # @return [RobotLab::Robot, RobotLab::Network]
62
+ def rebuild(config = AIA.config, history_mode: :clean)
63
+ old_robot = AIA.client
64
+ new_robot = build(config)
65
+
66
+ case history_mode
67
+ when :replay
68
+ HistoryTransfer.replay_history(old_robot, new_robot)
69
+ when :summarize
70
+ HistoryTransfer.summarize_history(old_robot, new_robot)
71
+ end
72
+
73
+ new_robot
74
+ end
75
+
76
+ # --- Forwarding wrappers for backward compatibility ---
77
+
78
+ def clear_tool_cache!
79
+ ToolLoader.clear_cache!
80
+ end
81
+
82
+ def filtered_tools(config)
83
+ ToolLoader.filtered_tools(config)
84
+ end
85
+
86
+ def resolve_system_prompt(config, model_spec = nil)
87
+ SystemPromptAssembler.resolve_system_prompt(config, model_spec)
88
+ end
89
+
90
+ def build_identity_prompt(robot_name, spec, roster)
91
+ SystemPromptAssembler.build_identity_prompt(robot_name, spec, roster)
92
+ end
93
+
94
+ # Build a concurrent MCP network where independent server groups
95
+ # run in parallel with a synthesizer to merge results.
96
+ #
97
+ # @param config [AIA::Config] the AIA configuration
98
+ # @param server_groups [Array<Array<Hash>>] groups of MCP server configs
99
+ # @return [RobotLab::Network]
100
+ def build_concurrent_mcp_network(config, server_groups)
101
+ namer = RobotNamer.new(first_name: 'Tobor')
102
+ NetworkBuilder.build_concurrent_mcp_network(config, namer, server_groups)
103
+ end
104
+
105
+ # Build a single robot for one model.
106
+ # Delegates to RobotBuilder for single-robot construction.
107
+ #
108
+ # @param config [AIA::Config] the AIA configuration
109
+ # @param namer [AIA::RobotNamer] fresh namer for this build
110
+ # @return [RobotLab::Robot]
111
+ def build_single_robot(config, namer)
112
+ RobotBuilder.build(config, namer: namer)
113
+ end
114
+
115
+ # Reuse the build-time tool cache when present, else load tools fresh.
116
+ def apply_tool_cache(config)
117
+ if ToolLoader.cached_tools
118
+ config.loaded_tools = ToolLoader.cached_tools
119
+ config.tool_names = ToolLoader.cached_tools.map { |t| t.respond_to?(:name) ? t.name : t.class.name }.join(', ')
120
+ else
121
+ ToolLoader.load_tools(config)
122
+ end
123
+ end
124
+
125
+ # Wrap a single robot in a one-member crew Network so the session can
126
+ # recruit/drop robots at runtime. The robot becomes the crew's chief
127
+ # (the lone pipeline task); normal turns are routed to it directly so
128
+ # token streaming is preserved.
129
+ #
130
+ # @param chief [RobotLab::Robot]
131
+ # @return [RobotLab::Network]
132
+ def build_crew(chief)
133
+ RobotLab.create_network(name: 'aia-crew') do
134
+ task chief.name.to_sym, chief, depends_on: :none
135
+ end
136
+ end
137
+
138
+ # Build RunConfig from AIA configuration.
139
+ # Uses the ruby_llm model registry to skip parameters the model doesn't support:
140
+ # - temperature is omitted for models that declare temperature: false in metadata
141
+ # - max_tokens remains a RobotLab::RunConfig field; AIA normalizes it at
142
+ # the RubyLLM/OpenAI provider boundary for models that need
143
+ # max_completion_tokens
144
+ def build_run_config(config)
145
+ model_spec = config.models.first
146
+ params = {}
147
+
148
+ temp = config.llm.temperature
149
+ params[:temperature] = temp if temp && model_supports_temperature?(model_spec)
150
+
151
+ max = config.llm.max_tokens
152
+ # Local providers (Ollama, LM Studio) have a small total context window
153
+ # (input + output combined, typically 32768 tokens for qwen3-class models).
154
+ # Sending the cloud default of 32767 as num_predict leaves almost no room
155
+ # once the input context grows across turns, causing mid-response truncation.
156
+ # Skip max_tokens for local providers when it's still at the cloud default;
157
+ # honour it when the user has explicitly set a smaller value.
158
+ local = model_spec&.local_provider?
159
+ params[:max_tokens] = max if max && !(local && max == DEFAULT_LOCAL_MAX_TOKENS)
160
+
161
+ tp = config.llm.top_p
162
+ params[:top_p] = tp if tp
163
+
164
+ fp = config.llm.frequency_penalty
165
+ params[:frequency_penalty] = fp if fp && fp != 0.0
166
+
167
+ pp = config.llm.presence_penalty
168
+ params[:presence_penalty] = pp if pp && pp != 0.0
169
+
170
+ RobotLab::RunConfig.new(**params)
171
+ end
172
+
173
+ # Returns false for models that explicitly declare temperature: false in the
174
+ # ruby_llm model registry (e.g. gpt-5.4, o1, o3 reasoning models).
175
+ def model_supports_temperature?(model_spec)
176
+ return true unless model_spec
177
+ info = RubyLLM.models.find(model_spec.name)
178
+ info.metadata[:temperature] != false
179
+ rescue StandardError
180
+ true
181
+ end
182
+
183
+ # Normalize all configured MCP server configs for robot_lab.
184
+ # Filtering/selection is MCPDiscovery's responsibility — this just normalizes shape.
185
+ def mcp_server_configs(config)
186
+ return [] if config.flags.no_mcp
187
+ Array(config.mcp_servers).map { |s| MCPConfigNormalizer.normalize(s) }
188
+ end
189
+
190
+ # Normalize a single MCP server config to robot_lab's nested transport format.
191
+ # Delegates to MCPConfigNormalizer.
192
+ def normalize_mcp_config(server)
193
+ MCPConfigNormalizer.normalize(server)
194
+ end
195
+
196
+ # Initialize shared memory for a network with session context.
197
+ # Delegates to NetworkMemoryManager.
198
+ #
199
+ # @param network [RobotLab::Network]
200
+ # @param config [AIA::Config]
201
+ # @return [RobotLab::Network]
202
+ def initialize_network_memory(network, config)
203
+ NetworkMemoryManager.initialize_memory(network, config)
204
+ end
205
+
206
+ # Set up memory subscriptions for debug logging and completion tracking.
207
+ # Delegates to NetworkMemoryManager.
208
+ #
209
+ # @param network [RobotLab::Network]
210
+ # @param config [AIA::Config]
211
+ def setup_memory_subscriptions(network, config)
212
+ NetworkMemoryManager.setup_subscriptions(network, config)
213
+ end
214
+
215
+ # Attach a shared TypedBus message bus to all robots in a network.
216
+ #
217
+ # @param network [RobotLab::Network]
218
+ # @return [TypedBus::MessageBus, nil]
219
+ def attach_bus(network)
220
+ return nil unless network.respond_to?(:robots)
221
+
222
+ bus = TypedBus::MessageBus.new
223
+ network.robots.each_value { |robot| robot.with_bus(bus) }
224
+ bus
225
+ end
226
+
227
+ # Build a RobotLab::Robot for a model spec, applying provider resolution.
228
+ # Centralizes the "set provider if the spec has one, then build" tail that
229
+ # every robot/network builder needs, so local-provider routing (ollama/lms)
230
+ # can't be forgotten at a new build site.
231
+ #
232
+ # @param spec [AIA::Config::ModelSpec] the model spec (supplies model: and provider)
233
+ # @param opts [Hash] forwarded to RobotLab.build (name:, system_prompt:,
234
+ # local_tools:, mcp_servers:, config:, on_content:, ...). Do NOT pass model:.
235
+ # @return [RobotLab::Robot]
236
+ def build_robot(spec, **opts)
237
+ opts[:provider] = resolve_provider(spec) if spec.provider
238
+ RobotLab.build(model: spec.name, **opts)
239
+ end
240
+
241
+ # Map AIA provider aliases to RubyLLM provider slugs.
242
+ def resolve_provider(model_spec)
243
+ case model_spec.provider
244
+ when 'lms' then 'openai'
245
+ else model_spec.provider
246
+ end
247
+ end
248
+
249
+ private
250
+
251
+ # Configure RobotLab providers from AIA config
252
+ def configure_robot_lab(config)
253
+ # Configure loggers before any API calls
254
+ AIA::LoggerManager.configure_llm_logger
255
+ AIA::LoggerManager.configure_mcp_logger
256
+
257
+ # Route RobotLab's internal logging through AIA's MCP logger
258
+ # so entries go to the configured log files instead of STDOUT.
259
+ # Must use direct assignment — block form doesn't set runtime attrs.
260
+ RobotLab.config.logger = AIA::LoggerManager.mcp_logger
261
+
262
+ RobotLab.config do |c|
263
+ c.ruby_llm do |r|
264
+ r.request_timeout = 120
265
+ end
266
+ end
267
+
268
+ # RobotLab's after_load sets PM.config.prompts_dir to its own default ('prompts').
269
+ # Restore AIA's configured prompts dir so PM.parse finds the correct files.
270
+ PM.configure { |c| c.prompts_dir = AIA.config.prompts.dir }
271
+
272
+ # Configure local provider API endpoints from environment variables
273
+ configure_local_providers(config)
274
+
275
+ # Wire audio model settings into RubyLLM config
276
+ configure_audio(config)
277
+ end
278
+
279
+ # Wire AIA's audio config into RubyLLM so transcription calls pick up
280
+ # the user-configured model without requiring explicit model: arguments.
281
+ # speech_model has no ruby_llm TTS API backing yet — it is exposed as
282
+ # the SPEECH_MODEL env var for custom speak_command scripts.
283
+ def configure_audio(config)
284
+ transcription_model = config.audio&.transcription_model
285
+ return unless transcription_model
286
+
287
+ RubyLLM.configure { |c| c.default_transcription_model = transcription_model }
288
+ end
289
+
290
+ # Set API base URLs for local providers (Ollama, LM Studio) so
291
+ # RubyLLM can reach them. Reads from standard env vars.
292
+ def configure_local_providers(config)
293
+ providers_used = config.models.map(&:provider).compact.uniq
294
+
295
+ RubyLLM.configure do |c|
296
+ # Always configure Ollama's base so switching to an `ollama/` model
297
+ # mid-session works even when the session started on a cloud model.
298
+ # It only affects the Ollama provider, so it's harmless when unused.
299
+ # RubyLLM's Ollama provider subclasses OpenAI and appends
300
+ # `chat/completions`, so the base must include the `/v1` suffix.
301
+ c.ollama_api_base = ENV.fetch('OLLAMA_API_BASE', 'http://localhost:11434/v1')
302
+
303
+ # LM Studio exposes an OpenAI-compatible API and reuses the OpenAI
304
+ # provider, so only override the OpenAI base when an `lms/` model is
305
+ # actually in use — otherwise it would hijack real OpenAI requests.
306
+ # The OpenAI provider POSTs the relative `chat/completions`, so (like
307
+ # the real `https://api.openai.com/v1` default and the Ollama base) the
308
+ # LM Studio base must include the `/v1` suffix — otherwise requests hit
309
+ # `:1234/chat/completions`, LM Studio 404s, and ruby_llm's streaming
310
+ # error handler masks it as `delete_prefix for nil`.
311
+ if providers_used.include?('lms')
312
+ c.openai_api_base = ENV.fetch('LMS_API_BASE', 'http://localhost:1234/v1')
313
+ end
314
+ end
315
+ end
316
+
317
+ # Decide between consensus and parallel multi-model.
318
+ # Initializes shared memory and subscriptions on the resulting network.
319
+ #
320
+ # @param config [AIA::Config] the AIA configuration
321
+ # @param namer [AIA::RobotNamer] fresh namer for this build
322
+ def build_multi_model(config, namer)
323
+ network = if config.flags.consensus
324
+ NetworkBuilder.build_consensus_network(config, namer)
325
+ else
326
+ NetworkBuilder.build_parallel_network(config, namer)
327
+ end
328
+ initialize_network_memory(network, config)
329
+ setup_memory_subscriptions(network, config)
330
+ network
331
+ end
332
+ end
333
+ end
334
+ end
@@ -0,0 +1,116 @@
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
+ @used_names[base] ||= 0
107
+ @used_names[base] += 1
108
+
109
+ if @used_names[base] == 1
110
+ base
111
+ else
112
+ "#{base}#{@used_names[base]}"
113
+ end
114
+ end
115
+ end
116
+ 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,19 @@ 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
22
26
  class Session
27
+ include ContentExtractor
28
+
23
29
  def initialize(prompt_handler)
24
30
  @prompt_handler = prompt_handler
25
31
 
@@ -30,6 +36,19 @@ module AIA
30
36
  # Starts the session, processing all prompts in the pipeline and then
31
37
  # optionally starting an interactive chat session.
32
38
  def start
39
+ # Build robot or network
40
+ @robot = RobotFactory.build(AIA.config)
41
+ AIA.client = @robot
42
+
43
+ # Run all startup coordination: MCP, tools, filters, task board, bus
44
+ coordinator = StartupCoordinator.new(robot: @robot, ui_presenter: @ui_presenter)
45
+ coordinator.run(AIA.config)
46
+ @filters = coordinator.filters
47
+ @mcp_manager = coordinator.mcp_manager
48
+
49
+ # Store session tracker globally for KBS access
50
+ AIA.session_tracker = @session_tracker
51
+
33
52
  # Handle special chat-only cases first
34
53
  if should_start_chat_immediately?
35
54
  AIA::Utility.robot
@@ -38,38 +57,85 @@ module AIA
38
57
  end
39
58
 
40
59
  # Process all prompts in the pipeline
41
- @prompt_pipeline.process_all
60
+ PipelineOrchestrator.new(
61
+ robot: @robot,
62
+ prompt_handler: @prompt_handler,
63
+ input_collector: @input_collector,
64
+ ui_presenter: @ui_presenter,
65
+ session_tracker: @session_tracker
66
+ ).process(AIA.config)
42
67
 
43
68
  # 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
69
+ return unless AIA.chat?
70
+ @chat_loop = build_chat_loop
71
+ AIA::Utility.robot
72
+ @ui_presenter.display_separator
73
+ @chat_loop.start(skip_context_files: true)
74
+ end
75
+
76
+ # Release all held resources: MCP connections and filter state.
77
+ # Called automatically via at_exit registered in #start.
78
+ # Safe to call multiple times.
79
+ def cleanup
80
+ @mcp_manager&.close_all
81
+ @filters&.each_value(&:cleanup)
49
82
  end
50
83
 
84
+ MAX_HISTORY_BYTES = 10 * 1024 * 1024 # 10 MB
85
+
51
86
  private
52
87
 
53
88
  def initialize_components
54
89
  @ui_presenter = UIPresenter.new
55
90
  @directive_processor = DirectiveProcessor.new
56
- @chat_processor = ChatProcessorService.new(@ui_presenter, @directive_processor)
57
91
  @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)
92
+
93
+ @session_tracker = SessionTracker.new
94
+ @alias_registry = ModelAliasRegistry.new(
95
+ AIA.config.respond_to?(:model_aliases) ? (AIA.config.model_aliases || {}) : {}
96
+ )
97
+ @chat_loop = nil # created after robot is built
98
+
99
+ rotate_history_log_if_needed
100
+ end
101
+
102
+ # Rotate the prompt history log if it has grown too large.
103
+ # Renames <file> to <file>.1 so the next session starts fresh.
104
+ # Called once at session initialization.
105
+ def rotate_history_log_if_needed
106
+ history_file = AIA.config.output.history_file
107
+ return unless history_file && File.exist?(history_file)
108
+ return unless File.size(history_file) > MAX_HISTORY_BYTES
109
+
110
+ archive = "#{history_file}.1"
111
+ FileUtils.mv(history_file, archive)
112
+ $stderr.puts "History log rotated: #{File.basename(history_file)} → #{File.basename(archive)}"
113
+ rescue StandardError => e
114
+ $stderr.puts "Warning: Could not rotate history log: #{e.message}"
60
115
  end
61
116
 
62
117
  def setup_output_file
63
118
  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
119
+ return unless out_file && !out_file.nil? && !AIA.append? && File.exist?(out_file)
120
+ File.write(out_file, '')
121
+ end
122
+
123
+ def build_chat_loop
124
+ ChatLoop.new(
125
+ @robot, @ui_presenter, @directive_processor,
126
+ session_tracker: @session_tracker,
127
+ alias_registry: @alias_registry,
128
+ filters: @filters
129
+ )
67
130
  end
68
131
 
69
132
  # Check if we should start chat immediately without processing any prompts
70
133
  def should_start_chat_immediately?
71
134
  return false unless AIA.chat?
72
135
 
136
+ # Create chat loop now that robot is available
137
+ @chat_loop = build_chat_loop
138
+
73
139
  AIA.config.pipeline.empty? || AIA.config.pipeline.all? { |id| id.nil? || id.empty? }
74
140
  end
75
141
  end