aidp 0.25.0 → 0.27.0
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/README.md +45 -6
- data/lib/aidp/analyze/error_handler.rb +11 -0
- data/lib/aidp/cli/checkpoint_command.rb +198 -0
- data/lib/aidp/cli/config_command.rb +71 -0
- data/lib/aidp/cli/enhanced_input.rb +2 -0
- data/lib/aidp/cli/first_run_wizard.rb +8 -7
- data/lib/aidp/cli/harness_command.rb +102 -0
- data/lib/aidp/cli/jobs_command.rb +3 -3
- data/lib/aidp/cli/mcp_dashboard.rb +4 -3
- data/lib/aidp/cli/models_command.rb +662 -0
- data/lib/aidp/cli/providers_command.rb +223 -0
- data/lib/aidp/cli.rb +35 -456
- data/lib/aidp/daemon/runner.rb +2 -2
- data/lib/aidp/debug_mixin.rb +2 -9
- data/lib/aidp/execute/async_work_loop_runner.rb +2 -1
- data/lib/aidp/execute/checkpoint_display.rb +38 -37
- data/lib/aidp/execute/interactive_repl.rb +2 -1
- data/lib/aidp/execute/prompt_manager.rb +4 -4
- data/lib/aidp/execute/work_loop_runner.rb +253 -56
- data/lib/aidp/execute/workflow_selector.rb +2 -2
- data/lib/aidp/harness/config_loader.rb +20 -11
- data/lib/aidp/harness/config_manager.rb +5 -5
- data/lib/aidp/harness/config_schema.rb +30 -8
- data/lib/aidp/harness/configuration.rb +105 -4
- data/lib/aidp/harness/enhanced_runner.rb +24 -15
- data/lib/aidp/harness/error_handler.rb +26 -5
- data/lib/aidp/harness/filter_strategy.rb +45 -0
- data/lib/aidp/harness/generic_filter_strategy.rb +63 -0
- data/lib/aidp/harness/model_cache.rb +269 -0
- data/lib/aidp/harness/model_discovery_service.rb +259 -0
- data/lib/aidp/harness/model_registry.rb +201 -0
- data/lib/aidp/harness/output_filter.rb +136 -0
- data/lib/aidp/harness/provider_manager.rb +18 -3
- data/lib/aidp/harness/rspec_filter_strategy.rb +82 -0
- data/lib/aidp/harness/runner.rb +5 -0
- data/lib/aidp/harness/test_runner.rb +165 -27
- data/lib/aidp/harness/thinking_depth_manager.rb +223 -7
- data/lib/aidp/harness/ui/enhanced_tui.rb +4 -1
- data/lib/aidp/logger.rb +35 -5
- data/lib/aidp/providers/adapter.rb +2 -4
- data/lib/aidp/providers/anthropic.rb +141 -128
- data/lib/aidp/providers/base.rb +98 -2
- data/lib/aidp/providers/capability_registry.rb +0 -1
- data/lib/aidp/providers/codex.rb +49 -67
- data/lib/aidp/providers/cursor.rb +71 -59
- data/lib/aidp/providers/gemini.rb +44 -60
- data/lib/aidp/providers/github_copilot.rb +2 -66
- data/lib/aidp/providers/kilocode.rb +24 -80
- data/lib/aidp/providers/opencode.rb +24 -80
- data/lib/aidp/safe_directory.rb +10 -3
- data/lib/aidp/setup/wizard.rb +345 -8
- data/lib/aidp/storage/csv_storage.rb +9 -3
- data/lib/aidp/storage/file_manager.rb +8 -2
- data/lib/aidp/storage/json_storage.rb +9 -3
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +40 -1
- data/lib/aidp/watch/change_request_processor.rb +659 -0
- data/lib/aidp/watch/plan_generator.rb +93 -14
- data/lib/aidp/watch/plan_processor.rb +71 -8
- data/lib/aidp/watch/repository_client.rb +85 -20
- data/lib/aidp/watch/review_processor.rb +3 -3
- data/lib/aidp/watch/runner.rb +37 -0
- data/lib/aidp/watch/state_store.rb +46 -1
- data/lib/aidp/workflows/guided_agent.rb +3 -3
- data/lib/aidp/workstream_executor.rb +5 -2
- data/lib/aidp.rb +4 -0
- data/templates/aidp-development.yml.example +2 -2
- data/templates/aidp-production.yml.example +3 -3
- data/templates/aidp.yml.example +53 -0
- metadata +14 -1
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "model_cache"
|
|
4
|
+
require_relative "model_registry"
|
|
5
|
+
|
|
6
|
+
module Aidp
|
|
7
|
+
module Harness
|
|
8
|
+
# Service for discovering available models from providers
|
|
9
|
+
#
|
|
10
|
+
# Orchestrates model discovery across multiple providers:
|
|
11
|
+
# 1. Checks cache first (with TTL)
|
|
12
|
+
# 2. Falls back to dynamic discovery via provider.discover_models
|
|
13
|
+
# 3. Merges with static registry for comprehensive results
|
|
14
|
+
# 4. Caches results for future use
|
|
15
|
+
#
|
|
16
|
+
# Usage:
|
|
17
|
+
# service = ModelDiscoveryService.new
|
|
18
|
+
# models = service.discover_models("anthropic")
|
|
19
|
+
# all_models = service.discover_all_models
|
|
20
|
+
class ModelDiscoveryService
|
|
21
|
+
attr_reader :cache, :registry
|
|
22
|
+
|
|
23
|
+
def initialize(cache: nil, registry: nil)
|
|
24
|
+
@cache = cache || ModelCache.new
|
|
25
|
+
@registry = registry || ModelRegistry.new
|
|
26
|
+
@provider_classes = discover_provider_classes
|
|
27
|
+
Aidp.log_debug("model_discovery_service", "initialized",
|
|
28
|
+
providers: @provider_classes.keys)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Discover models for a specific provider
|
|
32
|
+
#
|
|
33
|
+
# @param provider [String] Provider name (e.g., "anthropic", "cursor")
|
|
34
|
+
# @param use_cache [Boolean] Whether to use cached results (default: true)
|
|
35
|
+
# @return [Array<Hash>] Discovered models
|
|
36
|
+
def discover_models(provider, use_cache: true)
|
|
37
|
+
Aidp.log_info("model_discovery_service", "discovering models",
|
|
38
|
+
provider: provider, use_cache: use_cache)
|
|
39
|
+
|
|
40
|
+
# Check cache first
|
|
41
|
+
if use_cache
|
|
42
|
+
cached = @cache.get_cached_models(provider)
|
|
43
|
+
if cached
|
|
44
|
+
Aidp.log_debug("model_discovery_service", "using cached models",
|
|
45
|
+
provider: provider, count: cached.size)
|
|
46
|
+
return cached
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Perform discovery
|
|
51
|
+
models = perform_discovery(provider)
|
|
52
|
+
|
|
53
|
+
# Cache the results
|
|
54
|
+
@cache.cache_models(provider, models) if models.any?
|
|
55
|
+
|
|
56
|
+
models
|
|
57
|
+
rescue => e
|
|
58
|
+
Aidp.log_error("model_discovery_service", "discovery failed",
|
|
59
|
+
provider: provider, error: e.message, backtrace: e.backtrace.first(3))
|
|
60
|
+
[]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Discover models from all available providers
|
|
64
|
+
#
|
|
65
|
+
# @param use_cache [Boolean] Whether to use cached results
|
|
66
|
+
# @return [Hash] Hash of provider => models array
|
|
67
|
+
def discover_all_models(use_cache: true)
|
|
68
|
+
results = {}
|
|
69
|
+
|
|
70
|
+
@provider_classes.each_key do |provider|
|
|
71
|
+
models = discover_models(provider, use_cache: use_cache)
|
|
72
|
+
results[provider] = models if models.any?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
Aidp.log_info("model_discovery_service", "discovered all models",
|
|
76
|
+
providers: results.keys, total_models: results.values.flatten.size)
|
|
77
|
+
results
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Discover models concurrently from multiple providers
|
|
81
|
+
#
|
|
82
|
+
# @param providers [Array<String>] List of provider names
|
|
83
|
+
# @param use_cache [Boolean] Whether to use cached results
|
|
84
|
+
# @return [Hash] Hash of provider => models array
|
|
85
|
+
def discover_concurrent(providers, use_cache: true)
|
|
86
|
+
require "concurrent"
|
|
87
|
+
|
|
88
|
+
results = {}
|
|
89
|
+
mutex = Mutex.new
|
|
90
|
+
|
|
91
|
+
# Create a thread pool
|
|
92
|
+
pool = Concurrent::FixedThreadPool.new(providers.size)
|
|
93
|
+
|
|
94
|
+
# Submit discovery tasks
|
|
95
|
+
futures = providers.map do |provider|
|
|
96
|
+
Concurrent::Future.execute(executor: pool) do
|
|
97
|
+
models = discover_models(provider, use_cache: use_cache)
|
|
98
|
+
mutex.synchronize { results[provider] = models }
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Wait for all to complete
|
|
103
|
+
futures.each(&:wait)
|
|
104
|
+
|
|
105
|
+
pool.shutdown
|
|
106
|
+
pool.wait_for_termination(30)
|
|
107
|
+
|
|
108
|
+
Aidp.log_info("model_discovery_service", "concurrent discovery complete",
|
|
109
|
+
providers: results.keys, total_models: results.values.flatten.size)
|
|
110
|
+
results
|
|
111
|
+
rescue LoadError => e
|
|
112
|
+
# Fallback to sequential if concurrent gem not available
|
|
113
|
+
Aidp.log_warn("model_discovery_service", "concurrent gem not available, using sequential",
|
|
114
|
+
error: e.message)
|
|
115
|
+
providers.each_with_object({}) do |provider, hash|
|
|
116
|
+
hash[provider] = discover_models(provider, use_cache: use_cache)
|
|
117
|
+
end
|
|
118
|
+
rescue => e
|
|
119
|
+
Aidp.log_error("model_discovery_service", "concurrent discovery failed",
|
|
120
|
+
error: e.message)
|
|
121
|
+
{}
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Get all available models (discovery + static registry)
|
|
125
|
+
#
|
|
126
|
+
# Combines dynamically discovered models with static registry
|
|
127
|
+
#
|
|
128
|
+
# @param use_cache [Boolean] Whether to use cached results
|
|
129
|
+
# @return [Hash] Hash with :discovered and :registry keys
|
|
130
|
+
def all_available_models(use_cache: true)
|
|
131
|
+
discovered = discover_all_models(use_cache: use_cache)
|
|
132
|
+
registry_families = @registry.all_families
|
|
133
|
+
|
|
134
|
+
{
|
|
135
|
+
discovered: discovered,
|
|
136
|
+
registry: registry_families.map { |family| @registry.get_model_info(family) }.compact
|
|
137
|
+
}
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Find which providers support a given model family
|
|
141
|
+
#
|
|
142
|
+
# @param family_name [String] Model family name
|
|
143
|
+
# @return [Array<String>] List of provider names
|
|
144
|
+
def providers_supporting(family_name)
|
|
145
|
+
providers = []
|
|
146
|
+
|
|
147
|
+
@provider_classes.each do |provider_name, class_name|
|
|
148
|
+
provider_class = constantize_provider(class_name)
|
|
149
|
+
next unless provider_class
|
|
150
|
+
|
|
151
|
+
if provider_class.respond_to?(:supports_model_family?)
|
|
152
|
+
providers << provider_name if provider_class.supports_model_family?(family_name)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
providers
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Refresh cache for all providers
|
|
160
|
+
def refresh_all_caches
|
|
161
|
+
@cache.invalidate_all
|
|
162
|
+
discover_all_models(use_cache: false)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Refresh cache for specific provider
|
|
166
|
+
#
|
|
167
|
+
# @param provider [String] Provider name
|
|
168
|
+
def refresh_cache(provider)
|
|
169
|
+
@cache.invalidate(provider)
|
|
170
|
+
discover_models(provider, use_cache: false)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
def perform_discovery(provider)
|
|
176
|
+
provider_class = get_provider_class(provider)
|
|
177
|
+
unless provider_class
|
|
178
|
+
Aidp.log_warn("model_discovery_service", "unknown provider",
|
|
179
|
+
provider: provider)
|
|
180
|
+
return []
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
unless provider_class.respond_to?(:available?) && provider_class.available?
|
|
184
|
+
Aidp.log_debug("model_discovery_service", "provider not available",
|
|
185
|
+
provider: provider)
|
|
186
|
+
return []
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
unless provider_class.respond_to?(:discover_models)
|
|
190
|
+
Aidp.log_warn("model_discovery_service", "provider missing discover_models",
|
|
191
|
+
provider: provider)
|
|
192
|
+
return []
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
models = provider_class.discover_models
|
|
196
|
+
Aidp.log_info("model_discovery_service", "discovered models",
|
|
197
|
+
provider: provider, count: models.size)
|
|
198
|
+
models
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def get_provider_class(provider)
|
|
202
|
+
class_name = @provider_classes[provider]
|
|
203
|
+
return nil unless class_name
|
|
204
|
+
|
|
205
|
+
constantize_provider(class_name)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def constantize_provider(class_name)
|
|
209
|
+
# Safely constantize the provider class
|
|
210
|
+
parts = class_name.split("::")
|
|
211
|
+
parts.reduce(Object) { |mod, name| mod.const_get(name) }
|
|
212
|
+
rescue NameError => e
|
|
213
|
+
Aidp.log_debug("model_discovery_service", "provider class not found",
|
|
214
|
+
class: class_name, error: e.message)
|
|
215
|
+
nil
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Dynamically discover all provider classes from the providers directory
|
|
219
|
+
#
|
|
220
|
+
# @return [Hash] Hash of provider_name => class_name
|
|
221
|
+
def discover_provider_classes
|
|
222
|
+
providers_dir = File.join(__dir__, "../providers")
|
|
223
|
+
provider_files = Dir.glob("*.rb", base: providers_dir)
|
|
224
|
+
|
|
225
|
+
# Exclude base classes and utility files
|
|
226
|
+
excluded_files = ["base.rb", "adapter.rb", "error_taxonomy.rb", "capability_registry.rb"]
|
|
227
|
+
provider_files -= excluded_files
|
|
228
|
+
|
|
229
|
+
providers = {}
|
|
230
|
+
|
|
231
|
+
provider_files.each do |file|
|
|
232
|
+
provider_name = File.basename(file, ".rb")
|
|
233
|
+
# Convert to class name (e.g., "anthropic" -> "Anthropic", "github_copilot" -> "GithubCopilot")
|
|
234
|
+
class_name = provider_name.split("_").map(&:capitalize).join
|
|
235
|
+
full_class_name = "Aidp::Providers::#{class_name}"
|
|
236
|
+
|
|
237
|
+
# Try to load and verify the provider class exists
|
|
238
|
+
begin
|
|
239
|
+
require_relative "../providers/#{provider_name}"
|
|
240
|
+
provider_class = constantize_provider(full_class_name)
|
|
241
|
+
if provider_class&.respond_to?(:discover_models)
|
|
242
|
+
providers[provider_name] = full_class_name
|
|
243
|
+
end
|
|
244
|
+
rescue => e
|
|
245
|
+
# Skip providers that can't be loaded or don't implement discover_models
|
|
246
|
+
if ENV["DEBUG"]
|
|
247
|
+
Aidp.log_debug("model_discovery_service", "skipping provider",
|
|
248
|
+
provider: provider_name, reason: e.message)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
Aidp.log_debug("model_discovery_service", "discovered provider classes",
|
|
254
|
+
count: providers.size, providers: providers.keys)
|
|
255
|
+
providers
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
module Aidp
|
|
7
|
+
module Harness
|
|
8
|
+
# ModelRegistry manages the static registry of known model families and their tier classifications
|
|
9
|
+
#
|
|
10
|
+
# The registry uses model families (e.g., "claude-3-5-sonnet") rather than specific versioned
|
|
11
|
+
# model IDs (e.g., "claude-3-5-sonnet-20241022"). This design provides:
|
|
12
|
+
# - No version tracking burden - registry tracks families, not every dated version
|
|
13
|
+
# - Future-proofing - new model versions automatically inherit family tier
|
|
14
|
+
# - Provider autonomy - each provider handles version-specific naming
|
|
15
|
+
#
|
|
16
|
+
# Usage:
|
|
17
|
+
# registry = ModelRegistry.new
|
|
18
|
+
# registry.get_model_info("claude-3-5-sonnet")
|
|
19
|
+
# # => { name: "Claude 3.5 Sonnet", tier: "standard", ... }
|
|
20
|
+
#
|
|
21
|
+
# registry.models_for_tier("standard")
|
|
22
|
+
# # => ["claude-3-5-sonnet", "gpt-4-turbo", ...]
|
|
23
|
+
#
|
|
24
|
+
# registry.match_to_family("claude-3-5-sonnet-20241022")
|
|
25
|
+
# # => "claude-3-5-sonnet"
|
|
26
|
+
class ModelRegistry
|
|
27
|
+
class RegistryError < StandardError; end
|
|
28
|
+
class InvalidRegistrySchema < RegistryError; end
|
|
29
|
+
class ModelNotFound < RegistryError; end
|
|
30
|
+
|
|
31
|
+
VALID_TIERS = %w[mini standard advanced].freeze
|
|
32
|
+
VALID_CAPABILITIES = %w[chat code vision tool_use streaming json_mode].freeze
|
|
33
|
+
VALID_SPEEDS = %w[very_fast fast medium slow].freeze
|
|
34
|
+
|
|
35
|
+
attr_reader :registry_data
|
|
36
|
+
|
|
37
|
+
def initialize(registry_path: nil)
|
|
38
|
+
@registry_path = registry_path || default_registry_path
|
|
39
|
+
@registry_data = load_static_registry
|
|
40
|
+
validate_registry_schema
|
|
41
|
+
Aidp.log_debug("model_registry", "initialized", models: @registry_data["model_families"].keys.size)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Get complete model information for a family
|
|
45
|
+
#
|
|
46
|
+
# @param family_name [String] The model family name (e.g., "claude-3-5-sonnet")
|
|
47
|
+
# @return [Hash, nil] Model metadata hash or nil if not found
|
|
48
|
+
def get_model_info(family_name)
|
|
49
|
+
info = @registry_data["model_families"][family_name]
|
|
50
|
+
return nil unless info
|
|
51
|
+
|
|
52
|
+
info.merge("family" => family_name)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Get all model families for a specific tier
|
|
56
|
+
#
|
|
57
|
+
# @param tier [String, Symbol] The tier name (mini, standard, advanced)
|
|
58
|
+
# @return [Array<String>] List of model family names for the tier
|
|
59
|
+
def models_for_tier(tier)
|
|
60
|
+
tier_str = tier.to_s
|
|
61
|
+
unless VALID_TIERS.include?(tier_str)
|
|
62
|
+
Aidp.log_warn("model_registry", "invalid tier requested", tier: tier_str, valid: VALID_TIERS)
|
|
63
|
+
return []
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
families = @registry_data["model_families"].select { |_family, info|
|
|
67
|
+
info["tier"] == tier_str
|
|
68
|
+
}.keys
|
|
69
|
+
|
|
70
|
+
Aidp.log_debug("model_registry", "found models for tier", tier: tier_str, count: families.size)
|
|
71
|
+
families
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Classify a model family's tier
|
|
75
|
+
#
|
|
76
|
+
# @param family_name [String] The model family name
|
|
77
|
+
# @return [String, nil] The tier name or nil if not found
|
|
78
|
+
def classify_model_tier(family_name)
|
|
79
|
+
info = get_model_info(family_name)
|
|
80
|
+
tier = info&.fetch("tier", nil)
|
|
81
|
+
Aidp.log_debug("model_registry", "classified tier", family: family_name, tier: tier)
|
|
82
|
+
tier
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Match a versioned model name to its family using pattern matching
|
|
86
|
+
#
|
|
87
|
+
# This method attempts to normalize versioned model names (e.g., "claude-3-5-sonnet-20241022")
|
|
88
|
+
# to their family name (e.g., "claude-3-5-sonnet") by testing against version_pattern regexes.
|
|
89
|
+
#
|
|
90
|
+
# @param versioned_name [String] The versioned model name
|
|
91
|
+
# @return [String, nil] The family name if matched, nil otherwise
|
|
92
|
+
def match_to_family(versioned_name)
|
|
93
|
+
@registry_data["model_families"].each do |family, info|
|
|
94
|
+
pattern = info["version_pattern"]
|
|
95
|
+
next unless pattern
|
|
96
|
+
|
|
97
|
+
begin
|
|
98
|
+
regex = Regexp.new("^#{pattern}$")
|
|
99
|
+
if regex.match?(versioned_name)
|
|
100
|
+
Aidp.log_debug("model_registry", "matched to family", versioned: versioned_name, family: family)
|
|
101
|
+
return family
|
|
102
|
+
end
|
|
103
|
+
rescue RegexpError => e
|
|
104
|
+
Aidp.log_error("model_registry", "invalid pattern", family: family, pattern: pattern, error: e.message)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# If no pattern matches, check if the versioned_name is itself a family
|
|
109
|
+
if @registry_data["model_families"].key?(versioned_name)
|
|
110
|
+
Aidp.log_debug("model_registry", "exact family match", name: versioned_name)
|
|
111
|
+
return versioned_name
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
Aidp.log_debug("model_registry", "no family match", versioned: versioned_name)
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Get all registered model families
|
|
119
|
+
#
|
|
120
|
+
# @return [Array<String>] List of all model family names
|
|
121
|
+
def all_families
|
|
122
|
+
@registry_data["model_families"].keys
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Check if a model family exists in the registry
|
|
126
|
+
#
|
|
127
|
+
# @param family_name [String] The model family name
|
|
128
|
+
# @return [Boolean] True if the family exists
|
|
129
|
+
def family_exists?(family_name)
|
|
130
|
+
@registry_data["model_families"].key?(family_name)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Get all tiers that have at least one model
|
|
134
|
+
#
|
|
135
|
+
# @return [Array<String>] List of tier names
|
|
136
|
+
def available_tiers
|
|
137
|
+
@registry_data["model_families"].values.map { |info| info["tier"] }.uniq.sort
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
def default_registry_path
|
|
143
|
+
Pathname.new(__dir__).parent.join("data", "model_registry.yml")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def load_static_registry
|
|
147
|
+
unless File.exist?(@registry_path)
|
|
148
|
+
raise RegistryError, "Model registry file not found at #{@registry_path}"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
data = YAML.load_file(@registry_path)
|
|
152
|
+
unless data.is_a?(Hash) && data.key?("model_families")
|
|
153
|
+
raise InvalidRegistrySchema, "Registry must contain 'model_families' key"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
Aidp.log_info("model_registry", "loaded registry", path: @registry_path, families: data["model_families"].size)
|
|
157
|
+
data
|
|
158
|
+
rescue Psych::SyntaxError => e
|
|
159
|
+
raise InvalidRegistrySchema, "Invalid YAML in registry file: #{e.message}"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def validate_registry_schema
|
|
163
|
+
@registry_data["model_families"].each do |family, info|
|
|
164
|
+
validate_model_entry(family, info)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def validate_model_entry(family, info)
|
|
169
|
+
# Required fields
|
|
170
|
+
unless info["tier"]
|
|
171
|
+
raise InvalidRegistrySchema, "Model family '#{family}' missing required 'tier' field"
|
|
172
|
+
end
|
|
173
|
+
unless VALID_TIERS.include?(info["tier"])
|
|
174
|
+
raise InvalidRegistrySchema, "Model family '#{family}' has invalid tier '#{info["tier"]}'. Valid: #{VALID_TIERS.join(", ")}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Optional but validated if present
|
|
178
|
+
if info["capabilities"]
|
|
179
|
+
unless info["capabilities"].is_a?(Array)
|
|
180
|
+
raise InvalidRegistrySchema, "Model family '#{family}' capabilities must be an array"
|
|
181
|
+
end
|
|
182
|
+
invalid_caps = info["capabilities"] - VALID_CAPABILITIES
|
|
183
|
+
unless invalid_caps.empty?
|
|
184
|
+
Aidp.log_warn("model_registry", "unknown capabilities", family: family, unknown: invalid_caps)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
if info["speed"] && !VALID_SPEEDS.include?(info["speed"])
|
|
189
|
+
Aidp.log_warn("model_registry", "invalid speed", family: family, speed: info["speed"], valid: VALID_SPEEDS)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Validate numeric fields if present
|
|
193
|
+
%w[context_window max_output cost_per_1m_input cost_per_1m_output].each do |field|
|
|
194
|
+
if info[field] && !info[field].is_a?(Numeric)
|
|
195
|
+
raise InvalidRegistrySchema, "Model family '#{family}' field '#{field}' must be numeric"
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aidp
|
|
4
|
+
module Harness
|
|
5
|
+
# Filters test and linter output to reduce token consumption
|
|
6
|
+
# Uses framework-specific strategies to extract relevant information
|
|
7
|
+
class OutputFilter
|
|
8
|
+
# Output modes
|
|
9
|
+
MODES = {
|
|
10
|
+
full: :full, # No filtering (default for first run)
|
|
11
|
+
failures_only: :failures_only, # Only failure information
|
|
12
|
+
minimal: :minimal # Minimal failure info + summary
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
# @param config [Hash] Configuration options
|
|
16
|
+
# @option config [Symbol] :mode Output mode (:full, :failures_only, :minimal)
|
|
17
|
+
# @option config [Boolean] :include_context Include surrounding lines
|
|
18
|
+
# @option config [Integer] :context_lines Number of context lines
|
|
19
|
+
# @option config [Integer] :max_lines Maximum output lines
|
|
20
|
+
def initialize(config = {})
|
|
21
|
+
@mode = config[:mode] || :full
|
|
22
|
+
@include_context = config.fetch(:include_context, true)
|
|
23
|
+
@context_lines = config.fetch(:context_lines, 3)
|
|
24
|
+
@max_lines = config.fetch(:max_lines, 500)
|
|
25
|
+
|
|
26
|
+
validate_mode!
|
|
27
|
+
|
|
28
|
+
Aidp.log_debug("output_filter", "initialized",
|
|
29
|
+
mode: @mode,
|
|
30
|
+
include_context: @include_context,
|
|
31
|
+
max_lines: @max_lines)
|
|
32
|
+
rescue NameError
|
|
33
|
+
# Logging infrastructure not available in some tests
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Filter output based on framework and mode
|
|
37
|
+
# @param output [String] Raw output
|
|
38
|
+
# @param framework [Symbol] Framework identifier
|
|
39
|
+
# @return [String] Filtered output
|
|
40
|
+
def filter(output, framework: :unknown)
|
|
41
|
+
return output if @mode == :full
|
|
42
|
+
return "" if output.nil? || output.empty?
|
|
43
|
+
|
|
44
|
+
Aidp.log_debug("output_filter", "filtering_start",
|
|
45
|
+
framework: framework,
|
|
46
|
+
input_lines: output.lines.count)
|
|
47
|
+
|
|
48
|
+
strategy = strategy_for_framework(framework)
|
|
49
|
+
filtered = strategy.filter(output, self)
|
|
50
|
+
|
|
51
|
+
truncated = truncate_if_needed(filtered)
|
|
52
|
+
|
|
53
|
+
Aidp.log_debug("output_filter", "filtering_complete",
|
|
54
|
+
output_lines: truncated.lines.count,
|
|
55
|
+
reduction: reduction_stats(output, truncated))
|
|
56
|
+
|
|
57
|
+
truncated
|
|
58
|
+
rescue NameError
|
|
59
|
+
# Logging infrastructure not available
|
|
60
|
+
return output if @mode == :full
|
|
61
|
+
return "" if output.nil? || output.empty?
|
|
62
|
+
|
|
63
|
+
strategy = strategy_for_framework(framework)
|
|
64
|
+
filtered = strategy.filter(output, self)
|
|
65
|
+
truncate_if_needed(filtered)
|
|
66
|
+
rescue => e
|
|
67
|
+
# External failure - graceful degradation
|
|
68
|
+
begin
|
|
69
|
+
Aidp.log_error("output_filter", "filtering_failed",
|
|
70
|
+
framework: framework,
|
|
71
|
+
mode: @mode,
|
|
72
|
+
error: e.message,
|
|
73
|
+
error_class: e.class.name)
|
|
74
|
+
rescue NameError
|
|
75
|
+
# Logging not available
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Return original output as fallback
|
|
79
|
+
output
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Accessors for strategy use
|
|
83
|
+
attr_reader :mode, :include_context, :context_lines, :max_lines
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def validate_mode!
|
|
88
|
+
unless MODES.key?(@mode)
|
|
89
|
+
raise ArgumentError, "Invalid mode: #{@mode}. Must be one of #{MODES.keys}"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def strategy_for_framework(framework)
|
|
94
|
+
case framework
|
|
95
|
+
when :rspec
|
|
96
|
+
require_relative "rspec_filter_strategy"
|
|
97
|
+
RSpecFilterStrategy.new
|
|
98
|
+
when :minitest
|
|
99
|
+
require_relative "generic_filter_strategy"
|
|
100
|
+
GenericFilterStrategy.new
|
|
101
|
+
when :jest
|
|
102
|
+
require_relative "generic_filter_strategy"
|
|
103
|
+
GenericFilterStrategy.new
|
|
104
|
+
when :pytest
|
|
105
|
+
require_relative "generic_filter_strategy"
|
|
106
|
+
GenericFilterStrategy.new
|
|
107
|
+
else
|
|
108
|
+
require_relative "generic_filter_strategy"
|
|
109
|
+
GenericFilterStrategy.new
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def truncate_if_needed(output)
|
|
114
|
+
lines = output.lines
|
|
115
|
+
return output if lines.count <= @max_lines
|
|
116
|
+
|
|
117
|
+
truncated = lines.first(@max_lines).join
|
|
118
|
+
# Only add newline if truncated doesn't already end with one
|
|
119
|
+
separator = truncated.end_with?("\n") ? "" : "\n"
|
|
120
|
+
truncated + separator + "[Output truncated - #{lines.count - @max_lines} more lines omitted]"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def reduction_stats(input, output)
|
|
124
|
+
input_size = input.bytesize
|
|
125
|
+
output_size = output.bytesize
|
|
126
|
+
reduction = ((input_size - output_size).to_f / input_size * 100).round(1)
|
|
127
|
+
|
|
128
|
+
{
|
|
129
|
+
input_bytes: input_size,
|
|
130
|
+
output_bytes: output_size,
|
|
131
|
+
reduction_percent: reduction
|
|
132
|
+
}
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -1394,14 +1394,27 @@ module Aidp
|
|
|
1394
1394
|
|
|
1395
1395
|
# Execute a prompt with a specific provider
|
|
1396
1396
|
def execute_with_provider(provider_type, prompt, options = {})
|
|
1397
|
+
# Extract model from options if provided
|
|
1398
|
+
model_name = options.delete(:model)
|
|
1399
|
+
|
|
1397
1400
|
# Create provider factory instance
|
|
1398
1401
|
provider_factory = ProviderFactory.new
|
|
1399
1402
|
|
|
1403
|
+
# Add model to provider options if specified
|
|
1404
|
+
provider_options = options.dup
|
|
1405
|
+
provider_options[:model] = model_name if model_name
|
|
1406
|
+
|
|
1400
1407
|
# Create provider instance
|
|
1401
|
-
provider = provider_factory.create_provider(provider_type,
|
|
1408
|
+
provider = provider_factory.create_provider(provider_type, provider_options)
|
|
1402
1409
|
|
|
1403
|
-
# Set current provider
|
|
1410
|
+
# Set current provider and model
|
|
1404
1411
|
@current_provider = provider_type
|
|
1412
|
+
@current_model = model_name if model_name
|
|
1413
|
+
|
|
1414
|
+
Aidp.logger.debug("provider_manager", "Executing with provider",
|
|
1415
|
+
provider: provider_type,
|
|
1416
|
+
model: model_name,
|
|
1417
|
+
prompt_length: prompt.length)
|
|
1405
1418
|
|
|
1406
1419
|
# Execute the prompt with the provider
|
|
1407
1420
|
result = provider.send_message(prompt: prompt, session: nil)
|
|
@@ -1410,15 +1423,17 @@ module Aidp
|
|
|
1410
1423
|
{
|
|
1411
1424
|
status: "completed",
|
|
1412
1425
|
provider: provider_type,
|
|
1426
|
+
model: model_name,
|
|
1413
1427
|
output: result,
|
|
1414
1428
|
metadata: {
|
|
1415
1429
|
provider_type: provider_type,
|
|
1430
|
+
model: model_name,
|
|
1416
1431
|
prompt_length: prompt.length,
|
|
1417
1432
|
timestamp: Time.now.strftime("%Y-%m-%dT%H:%M:%S.%3N%z")
|
|
1418
1433
|
}
|
|
1419
1434
|
}
|
|
1420
1435
|
rescue => e
|
|
1421
|
-
log_rescue(e, component: "provider_manager", action: "execute_with_provider", fallback: "error_result", provider: provider_type, prompt_length: prompt.length)
|
|
1436
|
+
log_rescue(e, component: "provider_manager", action: "execute_with_provider", fallback: "error_result", provider: provider_type, model: model_name, prompt_length: prompt.length)
|
|
1422
1437
|
# Return error result
|
|
1423
1438
|
{
|
|
1424
1439
|
status: "error",
|