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
|
@@ -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
|
-
|
|
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 "
|
|
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 "
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
59
|
-
@
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|