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,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/aia/model_alias_registry.rb
4
+ #
5
+ # Configurable alias table that maps short names, provider names,
6
+ # and capability descriptors to model IDs.
7
+
8
+ module AIA
9
+ class ModelAliasRegistry
10
+ DEFAULT_ALIASES = {
11
+ # Short names
12
+ "claude" => "claude-sonnet-4-20250514",
13
+ "sonnet" => "claude-sonnet-4-20250514",
14
+ "opus" => "claude-opus-4-20250514",
15
+ "haiku" => "claude-haiku-4-5-20251001",
16
+ "gpt4" => "gpt-4o",
17
+ "gpt4o" => "gpt-4o",
18
+ "gpt4mini" => "gpt-4o-mini",
19
+ "gemini" => "gemini-2.0-flash",
20
+ "flash" => "gemini-2.0-flash",
21
+ "llama" => "llama-3.1-70b",
22
+
23
+ # Provider names (resolve to default model for provider)
24
+ "anthropic" => "claude-sonnet-4-20250514",
25
+ "openai" => "gpt-4o",
26
+ "google" => "gemini-2.0-flash",
27
+ "meta" => "llama-3.1-70b",
28
+
29
+ # Capability descriptors
30
+ "fast" => "claude-haiku-4-5-20251001",
31
+ "cheap" => "gpt-4o-mini",
32
+ "best" => "claude-opus-4-20250514",
33
+ "coding" => "claude-sonnet-4-20250514",
34
+ "vision" => "gpt-4o"
35
+ }.freeze
36
+
37
+ def initialize(custom_aliases = {})
38
+ @aliases = DEFAULT_ALIASES.merge(normalize_keys(custom_aliases))
39
+ end
40
+
41
+ # Resolve a single name or alias to a model ID.
42
+ #
43
+ # @param name_or_alias [String] model name, alias, or capability descriptor
44
+ # @return [String] resolved model ID
45
+ def resolve(name_or_alias)
46
+ normalized = normalize_key(name_or_alias)
47
+ @aliases[normalized] || fuzzy_match(name_or_alias) || name_or_alias.to_s.strip
48
+ end
49
+
50
+ # Resolve a string containing multiple model references.
51
+ # Handles comma-separated, "and"-separated, and mixed formats.
52
+ #
53
+ # @param input [String] e.g., "claude and gemini", "gpt4, llama"
54
+ # @return [Array<String>] resolved model IDs
55
+ def resolve_multiple(input)
56
+ names = input.to_s.split(/\s*(?:,|and|&|\+)\s*/i)
57
+ names.map { |n| resolve(n.strip) }.uniq
58
+ end
59
+
60
+ # Check if a name is a known alias
61
+ #
62
+ # @param name [String] the name to check
63
+ # @return [Boolean]
64
+ def known?(name)
65
+ @aliases.key?(normalize_key(name))
66
+ end
67
+
68
+ # List all known aliases
69
+ #
70
+ # @return [Hash<String, String>]
71
+ def all_aliases
72
+ @aliases.dup
73
+ end
74
+
75
+ private
76
+
77
+ def normalize_key(name)
78
+ name.to_s.strip.downcase.gsub(/[-_\s]+/, '')
79
+ end
80
+
81
+ def normalize_keys(hash)
82
+ hash.transform_keys { |k| normalize_key(k) }
83
+ end
84
+
85
+ def fuzzy_match(name)
86
+ return nil unless defined?(RubyLLM) && RubyLLM.respond_to?(:models)
87
+
88
+ target = name.to_s.downcase.strip
89
+ return nil if target.empty?
90
+
91
+ match = RubyLLM.models.find { |m| m.id.downcase.include?(target) }
92
+ match&.id
93
+ rescue StandardError
94
+ nil
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/aia/model_switch_handler.rb
4
+ #
5
+ # Handles natural language model-change intent detection.
6
+ # When a model-change intent is detected by the classification KB,
7
+ # extracts model names, confirms with the user, and reconfigures.
8
+
9
+ module AIA
10
+ class ModelSwitchHandler
11
+ include HandlerProtocol
12
+
13
+ def initialize(alias_registry, ui_presenter)
14
+ @aliases = alias_registry
15
+ @ui = ui_presenter
16
+ @model_exists_cache = {}
17
+ end
18
+
19
+ # Natural language model switching via KBS intent detection has been removed.
20
+ # Use the /model directive for model changes.
21
+ #
22
+ # @param context [HandlerContext]
23
+ # @return [Boolean] always false
24
+ def handle(context) # rubocop:disable Naming/PredicateMethod
25
+ false
26
+ end
27
+
28
+ private
29
+
30
+ def handle_switch(intent, config)
31
+ models = extract_model_names(intent[:raw_text])
32
+ return false if models.empty?
33
+
34
+ resolved = models.map { |m| @aliases.resolve(m) }
35
+ confirm_and_apply(config, resolved)
36
+ end
37
+
38
+ def handle_compare(intent, config) # rubocop:disable Naming/PredicateMethod
39
+ models = extract_model_names(intent[:raw_text])
40
+ return false if models.size < 2
41
+
42
+ resolved = models.map { |m| @aliases.resolve(m) }
43
+
44
+ @ui.display_info("Interpreted as: compare #{resolved.join(', ')}")
45
+ @ui.display_info("Proceed? (y/n)")
46
+
47
+ response = @ui.ask_question
48
+ return false unless response&.strip&.downcase == 'y'
49
+
50
+ config.flags.consensus = true
51
+ apply_model_change(config, resolved)
52
+ true
53
+ end
54
+
55
+ def handle_capability_switch(intent, config)
56
+ capability = intent[:capability]
57
+ resolved = [@aliases.resolve(capability)]
58
+ confirm_and_apply(config, resolved)
59
+ end
60
+
61
+ def confirm_and_apply(config, resolved) # rubocop:disable Naming/PredicateMethod
62
+ @ui.display_info("Interpreted as: /model #{resolved.join(', ')}")
63
+ @ui.display_info("Proceed? (y/n)")
64
+
65
+ response = @ui.ask_question
66
+ return false unless response&.strip&.downcase == 'y'
67
+
68
+ apply_model_change(config, resolved)
69
+ true
70
+ end
71
+
72
+ def extract_model_names(text)
73
+ return [] unless text
74
+
75
+ # Check each word against known aliases and model names
76
+ words = text.downcase.scan(/[\w.-]+/)
77
+ words.select { |w| @aliases.known?(w) || model_exists?(w) }.uniq
78
+ end
79
+
80
+ def model_exists?(name)
81
+ return @model_exists_cache[name] if @model_exists_cache.key?(name)
82
+ @model_exists_cache[name] = begin
83
+ return false unless defined?(RubyLLM) && RubyLLM.respond_to?(:models)
84
+ !!RubyLLM.models.find(name)
85
+ rescue StandardError
86
+ false
87
+ end
88
+ end
89
+
90
+ def apply_model_change(config, model_names)
91
+ config.models = model_names.map { |name| AIA::ModelSpec.new(name: name) }
92
+
93
+ history_mode = config.respond_to?(:model_switch_history) ?
94
+ config.model_switch_history&.to_sym : :clean
95
+
96
+ AIA.client = RobotFactory.rebuild(config, history_mode: history_mode)
97
+ @ui.display_info("Model switched to: #{model_names.join(', ')}")
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/aia/network_builder.rb
4
+ #
5
+ # Stateless module for building RobotLab::Network instances.
6
+ # Extracted from RobotFactory to reduce its size and isolate
7
+ # the network-specific construction logic.
8
+
9
+ module AIA
10
+ module NetworkBuilder
11
+ module_function
12
+
13
+ # Build a network where each prompt in the pipeline is a sequential step.
14
+ #
15
+ # @param config [AIA::Config]
16
+ # @param namer [RobotNamer]
17
+ # @return [RobotLab::Network]
18
+ def build_pipeline_network(config, namer)
19
+ prompts = config.pipeline
20
+ run_config = RobotFactory.build_run_config(config)
21
+ tools = ToolLoader.filtered_tools(config)
22
+ mcp = RobotFactory.mcp_server_configs(config)
23
+ model_spec = config.models.first
24
+
25
+ RobotLab.create_network(name: "aia-pipeline") do
26
+ prev = nil
27
+ prompts.each_with_index do |_prompt_id, i|
28
+ task_name = :"step_#{i}"
29
+ robot = RobotFactory.build_robot(
30
+ model_spec,
31
+ name: "#{namer.name_for(model_spec.name)}-s#{i}",
32
+ system_prompt: nil,
33
+ local_tools: tools,
34
+ mcp_servers: mcp,
35
+ config: run_config
36
+ )
37
+ task task_name, robot, depends_on: prev ? [prev] : :none
38
+ prev = task_name
39
+ end
40
+ end
41
+ end
42
+
43
+ # Build a network where multiple models run in parallel.
44
+ #
45
+ # @param config [AIA::Config]
46
+ # @param namer [RobotNamer]
47
+ # @return [RobotLab::Network]
48
+ def build_parallel_network(config, namer)
49
+ run_config = RobotFactory.build_run_config(config)
50
+ tools = ToolLoader.filtered_tools(config)
51
+ mcp = RobotFactory.mcp_server_configs(config)
52
+ aia_config = config
53
+ roster = config.models.map { |spec| { name: namer.name_for(spec.name), spec: spec } }
54
+ build_robot = ->(entry) { build_roster_robot(entry, roster, tools, mcp, run_config, aia_config) }
55
+
56
+ RobotLab.create_network(name: "aia-parallel") do
57
+ roster.each do |entry|
58
+ task entry[:spec].internal_id.to_sym, build_robot.call(entry), depends_on: :none
59
+ end
60
+ end
61
+ end
62
+
63
+ # Build a consensus network: all models run in parallel, then a
64
+ # synthesizer merges the results.
65
+ #
66
+ # @param config [AIA::Config]
67
+ # @param namer [RobotNamer]
68
+ # @return [RobotLab::Network]
69
+ def build_consensus_network(config, namer)
70
+ run_config = RobotFactory.build_run_config(config)
71
+ tools = ToolLoader.filtered_tools(config)
72
+ mcp = RobotFactory.mcp_server_configs(config)
73
+ primary = config.models.first
74
+ aia_config = config
75
+ roster = config.models.map { |spec| { name: namer.name_for(spec.name), spec: spec } }
76
+ build_robot = ->(entry) { build_roster_robot(entry, roster, tools, mcp, run_config, aia_config) }
77
+
78
+ RobotLab.create_network(name: "aia-consensus") do
79
+ roster.each do |entry|
80
+ task entry[:spec].internal_id.to_sym, build_robot.call(entry), depends_on: :none
81
+ end
82
+
83
+ synthesizer = RobotFactory.build_robot(
84
+ primary,
85
+ name: "Weaver",
86
+ system_prompt: "You are a synthesizer. Review the responses from multiple AI models " \
87
+ "and create a unified, coherent response that captures the best " \
88
+ "insights from each.",
89
+ config: run_config
90
+ )
91
+ task :consensus, synthesizer,
92
+ depends_on: config.models.map { |s| s.internal_id.to_sym }
93
+ end
94
+ end
95
+
96
+ # Shared helper: builds a single roster robot (used by parallel and consensus networks).
97
+ def build_roster_robot(entry, roster, tools, mcp, run_config, aia_config)
98
+ spec = entry[:spec]
99
+ identity = SystemPromptAssembler.build_identity_prompt(entry[:name], spec, roster)
100
+ base_prompt = SystemPromptAssembler.resolve_system_prompt(aia_config, spec)
101
+ system_prompt = [identity, base_prompt].compact.join("\n\n")
102
+ RobotFactory.build_robot(
103
+ spec,
104
+ name: entry[:name],
105
+ system_prompt: system_prompt,
106
+ local_tools: tools,
107
+ mcp_servers: mcp,
108
+ config: run_config
109
+ )
110
+ end
111
+
112
+ # Build a concurrent MCP network where independent server groups
113
+ # run in parallel with a synthesizer to merge results.
114
+ #
115
+ # @param config [AIA::Config]
116
+ # @param namer [RobotNamer]
117
+ # @param server_groups [Array<Array<Hash>>]
118
+ # @return [RobotLab::Network]
119
+ def build_concurrent_mcp_network(config, namer, server_groups)
120
+ run_config = RobotFactory.build_run_config(config)
121
+ tools = ToolLoader.filtered_tools(config)
122
+ model_spec = config.models.first
123
+ system_prompt = SystemPromptAssembler.resolve_system_prompt(config, model_spec)
124
+
125
+ network = RobotLab.create_network(name: "aia-concurrent-mcp") do
126
+ server_groups.each_with_index do |group, i|
127
+ robot = RobotFactory.build_robot(
128
+ model_spec,
129
+ name: "#{namer.name_for(model_spec.name)}-w#{i}",
130
+ system_prompt: system_prompt,
131
+ local_tools: tools,
132
+ mcp_servers: group.map { |s| RobotFactory.normalize_mcp_config(s) },
133
+ config: run_config
134
+ )
135
+ task :"mcp_worker_#{i}", robot, depends_on: :none
136
+ end
137
+
138
+ synthesizer = RobotFactory.build_robot(
139
+ model_spec,
140
+ name: "Weaver",
141
+ system_prompt: "You are a synthesizer. Merge the following results from " \
142
+ "multiple specialized queries into a coherent response. " \
143
+ "Preserve all relevant information. Resolve contradictions.",
144
+ config: run_config
145
+ )
146
+ task :synthesize, synthesizer,
147
+ depends_on: server_groups.each_index.map { |i| :"mcp_worker_#{i}" }
148
+ end
149
+
150
+ RobotFactory.initialize_network_memory(network, config)
151
+ RobotFactory.setup_memory_subscriptions(network, config)
152
+ network
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/aia/network_memory_manager.rb
4
+ #
5
+ # Manages shared memory initialization and subscriptions for
6
+ # multi-robot networks. Extracted from RobotFactory.
7
+
8
+ module AIA
9
+ class NetworkMemoryManager
10
+ class << self
11
+ # Initialize shared memory for a network with session context.
12
+ #
13
+ # @param network [RobotLab::Network]
14
+ # @param config [AIA::Config]
15
+ # @return [RobotLab::Network]
16
+ def initialize_memory(network, config)
17
+ return network unless network.respond_to?(:memory)
18
+
19
+ memory = network.memory
20
+ memory.data.session_id = SecureRandom.hex(8)
21
+ memory.data.model_count = config.models.size
22
+ memory.data.model_names = config.models.map(&:name)
23
+ memory.data.mode = config.flags.consensus ? :consensus : :parallel
24
+ memory.data.turn_count = 0
25
+
26
+ network
27
+ end
28
+
29
+ # Set up memory subscriptions for debug logging and completion tracking.
30
+ #
31
+ # @param network [RobotLab::Network]
32
+ # @param config [AIA::Config]
33
+ def setup_subscriptions(network, config)
34
+ return unless network.respond_to?(:memory)
35
+
36
+ memory = network.memory
37
+
38
+ if config.flags.debug
39
+ memory.subscribe_pattern("result_*") do |change|
40
+ AIA::LoggerManager.aia_logger.debug(
41
+ "Memory: #{change.key} by #{change.writer} at #{change.timestamp}"
42
+ )
43
+ end
44
+ end
45
+
46
+ memory.set(:completed_count, 0)
47
+ memory.subscribe_pattern("result_*") do |change|
48
+ next unless change.created?
49
+ count = memory.get(:completed_count) || 0
50
+ memory.set(:completed_count, count + 1)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,43 @@
1
+ # lib/aia/patches/ruby_llm_streaming_error.rb
2
+ #
3
+ # Patches RubyLLM::Streaming#handle_error_chunk to locate the SSE `data:` line
4
+ # robustly.
5
+ #
6
+ # The shipped implementation assumes the `data:` line is the SECOND line of the
7
+ # same chunk as `event: error`:
8
+ #
9
+ # error_data = chunk.split("\n")[1].delete_prefix('data: ')
10
+ #
11
+ # When a provider flushes `event: error` and its `data:` line in SEPARATE stream
12
+ # chunks (LM Studio does this on, e.g., a context-length overflow),
13
+ # chunk.split("\n")[1] is nil and the real provider message is masked as the
14
+ # cryptic `NoMethodError: undefined method 'delete_prefix' for nil`. This
15
+ # version scans the chunk for the `data:` line so the actual error propagates to
16
+ # the user. Falls back to the whole chunk when no data line is present, which
17
+ # still parses cleanly instead of raising.
18
+ #
19
+ # Applied via #prepend so it cleanly overrides the buggy method while reusing
20
+ # ruby_llm's own private parse_error_from_json. Guarded against API drift: if
21
+ # parse_error_from_json is renamed/removed the patch is skipped with a warning.
22
+ #
23
+ # NOTE: This fixes a genuine upstream bug — it should be contributed to
24
+ # crmne/ruby_llm so this patch can eventually be retired.
25
+
26
+ module AIA
27
+ module RubyLLMStreamingError
28
+ private
29
+
30
+ def handle_error_chunk(chunk, env)
31
+ data_line = chunk.to_s.lines.find { |line| line.start_with?('data:') }
32
+ error_data = data_line ? data_line.sub(/\Adata:\s*/, '').strip : chunk.to_s.strip
33
+ parse_error_from_json(error_data, env, 'Failed to parse error chunk')
34
+ end
35
+ end
36
+ end
37
+
38
+ if RubyLLM::Streaming.private_method_defined?(:parse_error_from_json)
39
+ RubyLLM::Streaming.prepend(AIA::RubyLLMStreamingError)
40
+ else
41
+ $stderr.puts "WARNING: [AIA] ruby_llm patch skipped: RubyLLM::Streaming#parse_error_from_json " \
42
+ "not found (ruby_llm #{RubyLLM::VERSION}). Streaming error messages may be cryptic."
43
+ end
@@ -0,0 +1,96 @@
1
+ # lib/aia/patches/ruby_llm_tool_error.rb
2
+ #
3
+ # Two ruby_llm hardening patches:
4
+ #
5
+ # 1. RubyLLM::Chat#execute_tool — rescue any exception raised during tool
6
+ # execution and return it as an error hash instead of raising. Without
7
+ # this, an exception inside a tool skips the add_message call in
8
+ # handle_tool_calls, leaving an orphaned tool_call in the conversation
9
+ # history with no matching tool_result. Most LLMs reject that state on the
10
+ # next request, killing the session. ruby_llm's own execute_tool does NOT
11
+ # rescue tool exceptions, so this is still required as of 1.16.
12
+ #
13
+ # 2. RubyLLM::Provider#complete — normalize OpenAI o-series / gpt-5 params
14
+ # (max_tokens -> max_completion_tokens) before the request.
15
+ #
16
+ # Both patches are applied via #prepend + super so they preserve ruby_llm's own
17
+ # method body (including its instrumentation) and survive internal changes to
18
+ # that body. They are guarded against drift: if a future ruby_llm renames or
19
+ # removes the target method, the patch is skipped with a loud warning rather
20
+ # than silently installing dead code or breaking every request.
21
+
22
+ module AIA
23
+ class OpenAIParameterNormalizer
24
+ class << self
25
+ def normalize(params, model, provider)
26
+ normalized = params.dup
27
+ return normalized unless uses_completion_tokens_param?(model, provider)
28
+ return normalized unless normalized.key?(:max_tokens)
29
+
30
+ max_tokens = normalized.delete(:max_tokens)
31
+ normalized[:max_completion_tokens] ||= max_tokens
32
+ normalized
33
+ end
34
+
35
+ private
36
+
37
+ def uses_completion_tokens_param?(model, provider)
38
+ provider_slug(provider) == 'openai' && model_id(model).match?(/\A(o\d|gpt-5)/i)
39
+ end
40
+
41
+ def provider_slug(provider)
42
+ provider.respond_to?(:slug) ? provider.slug.to_s : nil
43
+ end
44
+
45
+ def model_id(model)
46
+ model.respond_to?(:id) ? model.id.to_s : model.to_s
47
+ end
48
+ end
49
+ end
50
+
51
+ # Prepended onto RubyLLM::Provider. Uses a pass-through (*args, **kwargs)
52
+ # signature so any kwargs ruby_llm adds in future are forwarded to super
53
+ # unchanged; we only rewrite params before delegating.
54
+ module RubyLLMOpenAIParameters
55
+ def complete(*, **kwargs, &)
56
+ kwargs[:params] = AIA::OpenAIParameterNormalizer.normalize(
57
+ kwargs[:params] || {}, kwargs[:model], self
58
+ )
59
+ super
60
+ end
61
+ end
62
+
63
+ # Prepended onto RubyLLM::Chat. Calls super (ruby_llm's own instrumented
64
+ # execute_tool) and only adds exception rescue, so tool-call instrumentation
65
+ # is preserved.
66
+ module RubyLLMChatToolError
67
+ private
68
+
69
+ def execute_tool(tool_call)
70
+ super
71
+ rescue SignalException, SystemExit
72
+ raise
73
+ rescue Exception => e # rubocop:disable Lint/RescueException
74
+ RubyLLM.logger.warn { "Tool #{tool_call.name} raised #{e.class}: #{e.message}" }
75
+ { error: "#{e.class}: #{e.message}" }
76
+ end
77
+ end
78
+ end
79
+
80
+ # --- Apply patches, guarded against ruby_llm API drift ---
81
+
82
+ if RubyLLM::Provider.private_method_defined?(:complete) ||
83
+ RubyLLM::Provider.method_defined?(:complete)
84
+ RubyLLM::Provider.prepend(AIA::RubyLLMOpenAIParameters)
85
+ else
86
+ $stderr.puts "WARNING: [AIA] ruby_llm patch skipped: RubyLLM::Provider#complete " \
87
+ "not found (ruby_llm #{RubyLLM::VERSION}). OpenAI param normalization is off."
88
+ end
89
+
90
+ if RubyLLM::Chat.private_method_defined?(:execute_tool) ||
91
+ RubyLLM::Chat.method_defined?(:execute_tool)
92
+ RubyLLM::Chat.prepend(AIA::RubyLLMChatToolError)
93
+ else
94
+ $stderr.puts "WARNING: [AIA] ruby_llm patch skipped: RubyLLM::Chat#execute_tool " \
95
+ "not found (ruby_llm #{RubyLLM::VERSION}). Tool-exception handling is degraded."
96
+ end