aidp 0.28.0 → 0.30.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/lib/aidp/cli/models_command.rb +75 -117
- data/lib/aidp/cli/tools_command.rb +333 -0
- data/lib/aidp/cli.rb +8 -1
- data/lib/aidp/config.rb +9 -0
- data/lib/aidp/execute/work_loop_runner.rb +6 -3
- data/lib/aidp/harness/capability_registry.rb +4 -4
- data/lib/aidp/harness/deprecation_cache.rb +177 -0
- data/lib/aidp/harness/provider_manager.rb +5 -3
- data/lib/aidp/harness/ruby_llm_registry.rb +327 -0
- data/lib/aidp/harness/thinking_depth_manager.rb +47 -5
- data/lib/aidp/metadata/cache.rb +201 -0
- data/lib/aidp/metadata/compiler.rb +229 -0
- data/lib/aidp/metadata/parser.rb +204 -0
- data/lib/aidp/metadata/query.rb +237 -0
- data/lib/aidp/metadata/scanner.rb +191 -0
- data/lib/aidp/metadata/tool_metadata.rb +245 -0
- data/lib/aidp/metadata/validator.rb +187 -0
- data/lib/aidp/providers/aider.rb +1 -1
- data/lib/aidp/providers/anthropic.rb +189 -13
- data/lib/aidp/providers/base.rb +105 -35
- data/lib/aidp/providers/codex.rb +1 -1
- data/lib/aidp/providers/cursor.rb +1 -1
- data/lib/aidp/providers/gemini.rb +1 -1
- data/lib/aidp/providers/github_copilot.rb +1 -1
- data/lib/aidp/providers/kilocode.rb +1 -1
- data/lib/aidp/providers/opencode.rb +1 -1
- data/lib/aidp/setup/wizard.rb +35 -107
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp.rb +11 -0
- data/templates/implementation/implement_features.md +4 -1
- metadata +25 -2
- data/lib/aidp/harness/model_discovery_service.rb +0 -259
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../errors"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Metadata
|
|
7
|
+
# Validates tool metadata and detects issues
|
|
8
|
+
#
|
|
9
|
+
# Performs validation checks on tool metadata including:
|
|
10
|
+
# - Required field presence
|
|
11
|
+
# - Field type validation
|
|
12
|
+
# - Duplicate ID detection
|
|
13
|
+
# - Dependency resolution
|
|
14
|
+
# - Version format validation
|
|
15
|
+
#
|
|
16
|
+
# @example Validating a collection of tools
|
|
17
|
+
# validator = Validator.new(tools)
|
|
18
|
+
# results = validator.validate_all
|
|
19
|
+
# results.each { |r| puts "#{r[:file]}: #{r[:errors].join(", ")}" }
|
|
20
|
+
class Validator
|
|
21
|
+
# Validation result structure
|
|
22
|
+
ValidationResult = Struct.new(:tool_id, :file_path, :valid, :errors, :warnings, keyword_init: true)
|
|
23
|
+
|
|
24
|
+
# Initialize validator with tool metadata collection
|
|
25
|
+
#
|
|
26
|
+
# @param tools [Array<ToolMetadata>] Tools to validate
|
|
27
|
+
def initialize(tools = [])
|
|
28
|
+
@tools = tools
|
|
29
|
+
@errors_by_id = {}
|
|
30
|
+
@warnings_by_id = {}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Validate all tools
|
|
34
|
+
#
|
|
35
|
+
# @return [Array<ValidationResult>] Validation results for each tool
|
|
36
|
+
def validate_all
|
|
37
|
+
Aidp.log_debug("metadata", "Validating all tools", count: @tools.size)
|
|
38
|
+
|
|
39
|
+
results = @tools.map do |tool|
|
|
40
|
+
validate_tool(tool)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Cross-tool validations
|
|
44
|
+
validate_duplicate_ids(results)
|
|
45
|
+
validate_dependencies(results)
|
|
46
|
+
|
|
47
|
+
Aidp.log_info(
|
|
48
|
+
"metadata",
|
|
49
|
+
"Validation complete",
|
|
50
|
+
total: results.size,
|
|
51
|
+
valid: results.count(&:valid),
|
|
52
|
+
invalid: results.count { |r| !r.valid }
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
results
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Validate a single tool
|
|
59
|
+
#
|
|
60
|
+
# @param tool [ToolMetadata] Tool to validate
|
|
61
|
+
# @return [ValidationResult] Validation result
|
|
62
|
+
def validate_tool(tool)
|
|
63
|
+
errors = []
|
|
64
|
+
warnings = []
|
|
65
|
+
|
|
66
|
+
# Tool metadata validates itself on initialization
|
|
67
|
+
# Here we add additional cross-cutting validations
|
|
68
|
+
|
|
69
|
+
# Check for empty arrays in key fields
|
|
70
|
+
if tool.applies_to.empty? && tool.work_unit_types.empty?
|
|
71
|
+
warnings << "No applies_to tags or work_unit_types specified (tool may not be discoverable)"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Check for deprecated fields or patterns
|
|
75
|
+
validate_deprecated_patterns(tool, warnings)
|
|
76
|
+
|
|
77
|
+
# Check for experimental tools
|
|
78
|
+
if tool.experimental
|
|
79
|
+
warnings << "Tool is marked as experimental"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Check content length
|
|
83
|
+
if tool.content.length < 100
|
|
84
|
+
warnings << "Tool content is very short (#{tool.content.length} characters)"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
ValidationResult.new(
|
|
88
|
+
tool_id: tool.id,
|
|
89
|
+
file_path: tool.source_path,
|
|
90
|
+
valid: errors.empty?,
|
|
91
|
+
errors: errors,
|
|
92
|
+
warnings: warnings
|
|
93
|
+
)
|
|
94
|
+
rescue Aidp::Errors::ValidationError => e
|
|
95
|
+
# Catch validation errors from tool initialization
|
|
96
|
+
ValidationResult.new(
|
|
97
|
+
tool_id: tool&.id || "unknown",
|
|
98
|
+
file_path: tool&.source_path || "unknown",
|
|
99
|
+
valid: false,
|
|
100
|
+
errors: [e.message],
|
|
101
|
+
warnings: []
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Validate for duplicate IDs across tools
|
|
106
|
+
#
|
|
107
|
+
# @param results [Array<ValidationResult>] Validation results
|
|
108
|
+
def validate_duplicate_ids(results)
|
|
109
|
+
ids = @tools.map(&:id)
|
|
110
|
+
duplicates = ids.tally.select { |_, count| count > 1 }.keys
|
|
111
|
+
|
|
112
|
+
return if duplicates.empty?
|
|
113
|
+
|
|
114
|
+
duplicates.each do |dup_id|
|
|
115
|
+
matching_tools = @tools.select { |t| t.id == dup_id }
|
|
116
|
+
matching_tools.each do |tool|
|
|
117
|
+
result = results.find { |r| r.tool_id == tool.id && r.file_path == tool.source_path }
|
|
118
|
+
next unless result
|
|
119
|
+
|
|
120
|
+
paths = matching_tools.map(&:source_path).join(", ")
|
|
121
|
+
result.errors << "Duplicate ID '#{dup_id}' found in: #{paths}"
|
|
122
|
+
result.valid = false
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
Aidp.log_warn("metadata", "Duplicate IDs detected", duplicates: duplicates)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Validate tool dependencies are satisfied
|
|
130
|
+
#
|
|
131
|
+
# @param results [Array<ValidationResult>] Validation results
|
|
132
|
+
def validate_dependencies(results)
|
|
133
|
+
available_ids = @tools.map(&:id).to_set
|
|
134
|
+
|
|
135
|
+
@tools.each do |tool|
|
|
136
|
+
next if tool.dependencies.empty?
|
|
137
|
+
|
|
138
|
+
tool.dependencies.each do |dep_id|
|
|
139
|
+
next if available_ids.include?(dep_id)
|
|
140
|
+
|
|
141
|
+
result = results.find { |r| r.tool_id == tool.id && r.file_path == tool.source_path }
|
|
142
|
+
next unless result
|
|
143
|
+
|
|
144
|
+
result.errors << "Missing dependency: '#{dep_id}'"
|
|
145
|
+
result.valid = false
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Check for deprecated patterns in tool metadata
|
|
151
|
+
#
|
|
152
|
+
# @param tool [ToolMetadata] Tool to check
|
|
153
|
+
# @param warnings [Array<String>] Warnings array to append to
|
|
154
|
+
def validate_deprecated_patterns(tool, warnings)
|
|
155
|
+
# Check for legacy field usage (this would be expanded based on actual deprecations)
|
|
156
|
+
# For now, this is a placeholder for future deprecation warnings
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Write validation errors to log file
|
|
160
|
+
#
|
|
161
|
+
# @param results [Array<ValidationResult>] Validation results
|
|
162
|
+
# @param log_path [String] Path to error log file
|
|
163
|
+
def write_error_log(results, log_path)
|
|
164
|
+
Aidp.log_debug("metadata", "Writing error log", path: log_path)
|
|
165
|
+
|
|
166
|
+
errors = results.reject(&:valid)
|
|
167
|
+
return if errors.empty?
|
|
168
|
+
|
|
169
|
+
File.open(log_path, "w") do |f|
|
|
170
|
+
f.puts "# Metadata Validation Errors"
|
|
171
|
+
f.puts "# Generated: #{Time.now.iso8601}"
|
|
172
|
+
f.puts
|
|
173
|
+
|
|
174
|
+
errors.each do |result|
|
|
175
|
+
f.puts "## #{result.tool_id} (#{result.file_path})"
|
|
176
|
+
f.puts
|
|
177
|
+
result.errors.each { |err| f.puts "- ERROR: #{err}" }
|
|
178
|
+
result.warnings.each { |warn| f.puts "- WARNING: #{warn}" }
|
|
179
|
+
f.puts
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
Aidp.log_info("metadata", "Wrote error log", path: log_path, error_count: errors.size)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
data/lib/aidp/providers/aider.rb
CHANGED
|
@@ -75,7 +75,7 @@ module Aidp
|
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
def send_message(prompt:, session: nil)
|
|
78
|
+
def send_message(prompt:, session: nil, options: {})
|
|
79
79
|
raise "aider CLI not available" unless self.class.available?
|
|
80
80
|
|
|
81
81
|
# Smart timeout calculation (store prompt length for adaptive logic)
|
|
@@ -3,15 +3,23 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require_relative "base"
|
|
5
5
|
require_relative "../debug_mixin"
|
|
6
|
+
require_relative "../harness/deprecation_cache"
|
|
6
7
|
|
|
7
8
|
module Aidp
|
|
8
9
|
module Providers
|
|
9
10
|
class Anthropic < Base
|
|
10
11
|
include Aidp::DebugMixin
|
|
11
12
|
|
|
13
|
+
attr_reader :model
|
|
14
|
+
|
|
12
15
|
# Model name pattern for Anthropic Claude models
|
|
13
16
|
MODEL_PATTERN = /^claude-[\d.-]+-(?:opus|sonnet|haiku)(?:-\d{8})?$/i
|
|
14
17
|
|
|
18
|
+
# Get deprecation cache instance (lazy loaded)
|
|
19
|
+
def self.deprecation_cache
|
|
20
|
+
@deprecation_cache ||= Aidp::Harness::DeprecationCache.new
|
|
21
|
+
end
|
|
22
|
+
|
|
15
23
|
def self.available?
|
|
16
24
|
!!Aidp::Util.which("claude")
|
|
17
25
|
end
|
|
@@ -210,6 +218,56 @@ module Aidp
|
|
|
210
218
|
["--dangerously-skip-permissions"]
|
|
211
219
|
end
|
|
212
220
|
|
|
221
|
+
# Classify provider error using string matching
|
|
222
|
+
#
|
|
223
|
+
# ZFC EXCEPTION: Cannot use AI to classify provider errors because:
|
|
224
|
+
# 1. The failing provider IS the AI we'd use for classification (circular dependency)
|
|
225
|
+
# 2. Provider may be rate-limited, down, or misconfigured
|
|
226
|
+
# 3. Error classification must work even when AI unavailable
|
|
227
|
+
#
|
|
228
|
+
# This is a legitimate exception to ZFC principles per LLM_STYLE_GUIDE:
|
|
229
|
+
# "Structural safety checks" are allowed in code when AI cannot be used.
|
|
230
|
+
#
|
|
231
|
+
# @param error_message [String] The error message to classify
|
|
232
|
+
# @return [Hash] Classification result with :type, :is_deprecation, :is_rate_limit, :confidence
|
|
233
|
+
def self.classify_provider_error(error_message)
|
|
234
|
+
msg_lower = error_message.downcase
|
|
235
|
+
|
|
236
|
+
# Use simple string.include? checks (not regex) to avoid ReDoS vulnerabilities
|
|
237
|
+
is_rate_limit = msg_lower.include?("rate limit") || msg_lower.include?("session limit")
|
|
238
|
+
is_deprecation = msg_lower.include?("deprecat") || msg_lower.include?("end-of-life")
|
|
239
|
+
is_auth_error = msg_lower.include?("auth") && (msg_lower.include?("expired") || msg_lower.include?("invalid"))
|
|
240
|
+
|
|
241
|
+
# Determine primary type
|
|
242
|
+
type = if is_rate_limit
|
|
243
|
+
"rate_limit"
|
|
244
|
+
elsif is_deprecation
|
|
245
|
+
"deprecation"
|
|
246
|
+
elsif is_auth_error
|
|
247
|
+
"auth_error"
|
|
248
|
+
else
|
|
249
|
+
"other"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
Aidp.log_debug("anthropic", "Provider error classification",
|
|
253
|
+
type: type,
|
|
254
|
+
is_rate_limit: is_rate_limit,
|
|
255
|
+
is_deprecation: is_deprecation,
|
|
256
|
+
is_auth_error: is_auth_error)
|
|
257
|
+
|
|
258
|
+
{
|
|
259
|
+
type: type,
|
|
260
|
+
is_rate_limit: is_rate_limit,
|
|
261
|
+
is_deprecation: is_deprecation,
|
|
262
|
+
is_auth_error: is_auth_error,
|
|
263
|
+
confidence: 0.85, # Good confidence for clear error messages
|
|
264
|
+
reasoning: "Pattern-based classification (ZFC exception: circular dependency)"
|
|
265
|
+
}
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Error patterns for classify_error method (legacy support)
|
|
269
|
+
# NOTE: ZFC-based classification preferred - see classify_error_with_zfc
|
|
270
|
+
# These patterns serve as fallback when ZFC is unavailable
|
|
213
271
|
def error_patterns
|
|
214
272
|
{
|
|
215
273
|
rate_limited: [
|
|
@@ -245,18 +303,76 @@ module Aidp
|
|
|
245
303
|
/not.*found/i,
|
|
246
304
|
/404/,
|
|
247
305
|
/bad.*request/i,
|
|
248
|
-
/400
|
|
306
|
+
/400/,
|
|
307
|
+
/model.*deprecated/i,
|
|
308
|
+
/end-of-life/i
|
|
249
309
|
]
|
|
250
310
|
}
|
|
251
311
|
end
|
|
252
312
|
|
|
253
|
-
|
|
313
|
+
# Check if a model is deprecated and return replacement
|
|
314
|
+
# @param model_name [String] The model name to check
|
|
315
|
+
# @return [String, nil] Replacement model name if deprecated, nil otherwise
|
|
316
|
+
def self.check_model_deprecation(model_name)
|
|
317
|
+
deprecation_cache.replacement_for(provider: "anthropic", model_id: model_name)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Find replacement model for deprecated one using RubyLLM registry
|
|
321
|
+
# @param deprecated_model [String] The deprecated model name
|
|
322
|
+
# @param provider_name [String] Provider name for registry lookup
|
|
323
|
+
# @return [String, nil] Latest model in the same family, or configured replacement
|
|
324
|
+
def self.find_replacement_model(deprecated_model, provider_name: "anthropic")
|
|
325
|
+
# First check the deprecation cache for explicit replacement
|
|
326
|
+
replacement = deprecation_cache.replacement_for(provider: provider_name, model_id: deprecated_model)
|
|
327
|
+
return replacement if replacement
|
|
328
|
+
|
|
329
|
+
# Try to find latest model in same family using registry
|
|
330
|
+
require_relative "../harness/ruby_llm_registry" unless defined?(Aidp::Harness::RubyLLMRegistry)
|
|
331
|
+
|
|
332
|
+
begin
|
|
333
|
+
registry = Aidp::Harness::RubyLLMRegistry.new
|
|
334
|
+
|
|
335
|
+
# Search for non-deprecated models in the same family
|
|
336
|
+
# Prefer models without "latest" suffix, sorted by ID (newer dates first)
|
|
337
|
+
models = registry.models_for_provider(provider_name)
|
|
338
|
+
candidates = models.select { |m| m.include?("sonnet") && !deprecation_cache.deprecated?(provider: provider_name, model_id: m) }
|
|
339
|
+
|
|
340
|
+
# Prioritize: versioned models over -latest, newer versions first
|
|
341
|
+
versioned = candidates.reject { |m| m.end_with?("-latest") }.sort.reverse
|
|
342
|
+
latest_models = candidates.select { |m| m.end_with?("-latest") }
|
|
343
|
+
|
|
344
|
+
replacement = versioned.first || latest_models.first
|
|
345
|
+
|
|
346
|
+
if replacement
|
|
347
|
+
Aidp.log_info("anthropic", "Found replacement model",
|
|
348
|
+
deprecated: deprecated_model,
|
|
349
|
+
replacement: replacement)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
replacement
|
|
353
|
+
rescue => e
|
|
354
|
+
Aidp.log_error("anthropic", "Failed to find replacement model",
|
|
355
|
+
deprecated: deprecated_model,
|
|
356
|
+
error: e.message)
|
|
357
|
+
nil
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def send_message(prompt:, session: nil, options: {})
|
|
254
362
|
raise "claude CLI not available" unless self.class.available?
|
|
255
363
|
|
|
256
|
-
#
|
|
257
|
-
|
|
364
|
+
# Check if current model is deprecated and warn
|
|
365
|
+
if @model && (replacement = self.class.check_model_deprecation(@model))
|
|
366
|
+
Aidp.log_warn("anthropic", "Using deprecated model",
|
|
367
|
+
current: @model,
|
|
368
|
+
replacement: replacement)
|
|
369
|
+
debug_log("⚠️ Model #{@model} is deprecated. Consider upgrading to #{replacement}", level: :warn)
|
|
370
|
+
end
|
|
258
371
|
|
|
259
|
-
|
|
372
|
+
# Smart timeout calculation with tier awareness
|
|
373
|
+
timeout_seconds = calculate_timeout(options)
|
|
374
|
+
|
|
375
|
+
debug_provider("claude", "Starting execution", {timeout: timeout_seconds, tier: options[:tier]})
|
|
260
376
|
debug_log("📝 Sending prompt to claude...", level: :info)
|
|
261
377
|
|
|
262
378
|
# Build command arguments
|
|
@@ -285,15 +401,72 @@ module Aidp
|
|
|
285
401
|
# Detect issues in stdout/stderr (Claude sometimes prints to stdout)
|
|
286
402
|
combined = [result.out, result.err].compact.join("\n")
|
|
287
403
|
|
|
288
|
-
#
|
|
289
|
-
|
|
404
|
+
# Classify provider error using pattern matching
|
|
405
|
+
# ZFC EXCEPTION: Cannot use AI to classify provider's own errors (circular dependency)
|
|
406
|
+
error_classification = self.class.classify_provider_error(combined)
|
|
407
|
+
|
|
408
|
+
Aidp.log_debug("anthropic_provider", "error_classified",
|
|
409
|
+
exit_code: result.exit_status,
|
|
410
|
+
type: error_classification[:type],
|
|
411
|
+
confidence: error_classification[:confidence]) # Check for rate limit
|
|
412
|
+
if error_classification[:is_rate_limit]
|
|
290
413
|
Aidp.log_debug("anthropic_provider", "rate_limit_detected",
|
|
291
414
|
exit_code: result.exit_status,
|
|
415
|
+
confidence: error_classification[:confidence],
|
|
292
416
|
message: combined)
|
|
293
417
|
notify_rate_limit(combined)
|
|
294
418
|
error_message = "Rate limit reached for Claude CLI.\n#{combined}"
|
|
295
|
-
|
|
296
|
-
|
|
419
|
+
error = RuntimeError.new(error_message)
|
|
420
|
+
debug_error(error, {exit_code: result.exit_status, stdout: result.out, stderr: result.err})
|
|
421
|
+
raise error
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Check for model deprecation
|
|
425
|
+
if error_classification[:is_deprecation]
|
|
426
|
+
deprecated_model = @model
|
|
427
|
+
Aidp.log_error("anthropic", "Model deprecation detected",
|
|
428
|
+
model: deprecated_model,
|
|
429
|
+
message: combined)
|
|
430
|
+
|
|
431
|
+
# Try to find replacement
|
|
432
|
+
replacement = deprecated_model ? self.class.find_replacement_model(deprecated_model) : nil
|
|
433
|
+
|
|
434
|
+
# Record deprecation in cache for future runs
|
|
435
|
+
if replacement
|
|
436
|
+
self.class.deprecation_cache.add_deprecated_model(
|
|
437
|
+
provider: "anthropic",
|
|
438
|
+
model_id: deprecated_model,
|
|
439
|
+
replacement: replacement,
|
|
440
|
+
reason: combined.lines.first&.strip || "Model deprecated"
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
Aidp.log_info("anthropic", "Auto-upgrading to non-deprecated model",
|
|
444
|
+
old_model: deprecated_model,
|
|
445
|
+
new_model: replacement)
|
|
446
|
+
debug_log("🔄 Upgrading from deprecated model #{deprecated_model} to #{replacement}", level: :info)
|
|
447
|
+
|
|
448
|
+
# Update model and retry
|
|
449
|
+
@model = replacement
|
|
450
|
+
|
|
451
|
+
# Retry with new model
|
|
452
|
+
debug_log("🔄 Retrying with upgraded model: #{replacement}", level: :info)
|
|
453
|
+
return send_message(prompt: prompt, session: session, options: options)
|
|
454
|
+
else
|
|
455
|
+
# Record deprecation even without replacement
|
|
456
|
+
if deprecated_model
|
|
457
|
+
self.class.deprecation_cache.add_deprecated_model(
|
|
458
|
+
provider: "anthropic",
|
|
459
|
+
model_id: deprecated_model,
|
|
460
|
+
replacement: nil,
|
|
461
|
+
reason: combined.lines.first&.strip || "Model deprecated"
|
|
462
|
+
)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
error_message = "Model '#{deprecated_model}' is deprecated and no replacement found.\n#{combined}"
|
|
466
|
+
error = RuntimeError.new(error_message)
|
|
467
|
+
debug_error(error, {exit_code: result.exit_status, stdout: result.out, stderr: result.err})
|
|
468
|
+
raise error
|
|
469
|
+
end
|
|
297
470
|
end
|
|
298
471
|
|
|
299
472
|
# Check for auth issues
|
|
@@ -301,12 +474,15 @@ module Aidp
|
|
|
301
474
|
error_message = "Authentication error from Claude CLI: token expired or invalid.\n" \
|
|
302
475
|
"Run 'claude /login' or refresh credentials.\n" \
|
|
303
476
|
"Note: Model discovery requires valid authentication."
|
|
304
|
-
|
|
305
|
-
|
|
477
|
+
error = RuntimeError.new(error_message)
|
|
478
|
+
debug_error(error, {exit_code: result.exit_status, stdout: result.out, stderr: result.err})
|
|
479
|
+
raise error
|
|
306
480
|
end
|
|
307
481
|
|
|
308
|
-
|
|
309
|
-
|
|
482
|
+
error_message = "claude failed with exit code #{result.exit_status}: #{result.err}"
|
|
483
|
+
error = RuntimeError.new(error_message)
|
|
484
|
+
debug_error(error, {exit_code: result.exit_status, stderr: result.err})
|
|
485
|
+
raise error
|
|
310
486
|
end
|
|
311
487
|
rescue => e
|
|
312
488
|
debug_error(e, {provider: "claude", prompt_length: prompt.length})
|
data/lib/aidp/providers/base.rb
CHANGED
|
@@ -37,6 +37,16 @@ module Aidp
|
|
|
37
37
|
TIMEOUT_REFACTORING_RECOMMENDATIONS = 600 # 10 minutes - refactoring
|
|
38
38
|
TIMEOUT_IMPLEMENTATION = 900 # 15 minutes - implementation (write files, run tests, fix issues)
|
|
39
39
|
|
|
40
|
+
# Tier-based timeout multipliers (applied on top of base timeouts)
|
|
41
|
+
# Higher tiers need more time for deeper reasoning
|
|
42
|
+
TIER_TIMEOUT_MULTIPLIERS = {
|
|
43
|
+
"mini" => 1.0, # 5 minutes default (300s base)
|
|
44
|
+
"standard" => 2.0, # 10 minutes default (600s base)
|
|
45
|
+
"thinking" => 6.0, # 30 minutes default (1800s base)
|
|
46
|
+
"pro" => 6.0, # 30 minutes default (1800s base)
|
|
47
|
+
"max" => 12.0 # 60 minutes default (3600s base)
|
|
48
|
+
}.freeze
|
|
49
|
+
|
|
40
50
|
attr_reader :activity_state, :last_activity_time, :start_time, :step_name, :model
|
|
41
51
|
|
|
42
52
|
def initialize(output: nil, prompt: TTY::Prompt.new)
|
|
@@ -78,10 +88,12 @@ module Aidp
|
|
|
78
88
|
# Configure the provider with options
|
|
79
89
|
# @param config [Hash] Configuration options, may include :model
|
|
80
90
|
def configure(config)
|
|
81
|
-
|
|
91
|
+
if config[:model]
|
|
92
|
+
@model = resolve_model_name(config[:model].to_s)
|
|
93
|
+
end
|
|
82
94
|
end
|
|
83
95
|
|
|
84
|
-
def send_message(prompt:, session: nil)
|
|
96
|
+
def send_message(prompt:, session: nil, options: {})
|
|
85
97
|
raise NotImplementedError, "#{self.class} must implement #send_message"
|
|
86
98
|
end
|
|
87
99
|
|
|
@@ -482,11 +494,12 @@ module Aidp
|
|
|
482
494
|
# Priority order:
|
|
483
495
|
# 1. Quick mode (for testing)
|
|
484
496
|
# 2. Provider-specific environment variable override
|
|
485
|
-
# 3. Adaptive timeout based on step type
|
|
486
|
-
# 4. Default timeout
|
|
497
|
+
# 3. Adaptive timeout based on step type and thinking tier
|
|
498
|
+
# 4. Default timeout (with tier multiplier)
|
|
487
499
|
#
|
|
488
500
|
# Override provider_env_var to customize the environment variable name
|
|
489
|
-
|
|
501
|
+
# @param options [Hash] Options hash that may include :tier
|
|
502
|
+
def calculate_timeout(options = {})
|
|
490
503
|
if ENV["AIDP_QUICK_MODE"]
|
|
491
504
|
display_message("⚡ Quick mode enabled - #{TIMEOUT_QUICK_MODE / 60} minute timeout", type: :highlight)
|
|
492
505
|
return TIMEOUT_QUICK_MODE
|
|
@@ -495,47 +508,104 @@ module Aidp
|
|
|
495
508
|
provider_env_var = "AIDP_#{name.upcase}_TIMEOUT"
|
|
496
509
|
return ENV[provider_env_var].to_i if ENV[provider_env_var]
|
|
497
510
|
|
|
498
|
-
|
|
511
|
+
tier = options[:tier]&.to_s
|
|
512
|
+
step_timeout = adaptive_timeout(tier)
|
|
499
513
|
if step_timeout
|
|
500
|
-
|
|
514
|
+
tier_label = tier ? " (tier: #{tier})" : ""
|
|
515
|
+
display_message("🧠 Using adaptive timeout: #{step_timeout} seconds#{tier_label}", type: :info)
|
|
501
516
|
return step_timeout
|
|
502
517
|
end
|
|
503
518
|
|
|
504
|
-
# Default timeout
|
|
505
|
-
|
|
506
|
-
|
|
519
|
+
# Default timeout with tier multiplier
|
|
520
|
+
base_timeout = TIMEOUT_DEFAULT
|
|
521
|
+
final_timeout = apply_tier_multiplier(base_timeout, tier)
|
|
522
|
+
tier_label = tier ? " (tier: #{tier})" : ""
|
|
523
|
+
display_message("📋 Using default timeout: #{final_timeout / 60} minutes#{tier_label}", type: :info)
|
|
524
|
+
final_timeout
|
|
507
525
|
end
|
|
508
526
|
|
|
509
|
-
# Get adaptive timeout based on step type
|
|
527
|
+
# Get adaptive timeout based on step type and thinking tier
|
|
510
528
|
#
|
|
511
529
|
# This method returns different timeout values based on the type of operation
|
|
512
|
-
# being performed, as indicated by the AIDP_CURRENT_STEP environment variable
|
|
530
|
+
# being performed, as indicated by the AIDP_CURRENT_STEP environment variable,
|
|
531
|
+
# and applies a multiplier based on the thinking tier (mini/standard/thinking/pro/max).
|
|
513
532
|
# Returns nil for unknown steps to allow calculate_timeout to use the default.
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
533
|
+
#
|
|
534
|
+
# @param tier [String, nil] The thinking tier (mini, standard, thinking, pro, max)
|
|
535
|
+
def adaptive_timeout(tier = nil)
|
|
536
|
+
# Don't cache - tier may change between calls
|
|
537
|
+
step_name = ENV["AIDP_CURRENT_STEP"] || ""
|
|
538
|
+
|
|
539
|
+
base_timeout = case step_name
|
|
540
|
+
when /REPOSITORY_ANALYSIS/
|
|
541
|
+
TIMEOUT_REPOSITORY_ANALYSIS
|
|
542
|
+
when /ARCHITECTURE_ANALYSIS/
|
|
543
|
+
TIMEOUT_ARCHITECTURE_ANALYSIS
|
|
544
|
+
when /TEST_ANALYSIS/
|
|
545
|
+
TIMEOUT_TEST_ANALYSIS
|
|
546
|
+
when /FUNCTIONALITY_ANALYSIS/
|
|
547
|
+
TIMEOUT_FUNCTIONALITY_ANALYSIS
|
|
548
|
+
when /DOCUMENTATION_ANALYSIS/
|
|
549
|
+
TIMEOUT_DOCUMENTATION_ANALYSIS
|
|
550
|
+
when /STATIC_ANALYSIS/
|
|
551
|
+
TIMEOUT_STATIC_ANALYSIS
|
|
552
|
+
when /REFACTORING_RECOMMENDATIONS/
|
|
553
|
+
TIMEOUT_REFACTORING_RECOMMENDATIONS
|
|
554
|
+
when /IMPLEMENTATION/
|
|
555
|
+
TIMEOUT_IMPLEMENTATION
|
|
556
|
+
else
|
|
557
|
+
nil # Return nil for unknown steps
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
return nil unless base_timeout
|
|
561
|
+
|
|
562
|
+
apply_tier_multiplier(base_timeout, tier)
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# Apply tier-based multiplier to a base timeout
|
|
566
|
+
#
|
|
567
|
+
# @param base_timeout [Integer] The base timeout in seconds
|
|
568
|
+
# @param tier [String, nil] The thinking tier (mini, standard, thinking, pro, max)
|
|
569
|
+
# @return [Integer] The adjusted timeout with tier multiplier applied
|
|
570
|
+
def apply_tier_multiplier(base_timeout, tier)
|
|
571
|
+
return base_timeout unless tier
|
|
572
|
+
|
|
573
|
+
multiplier = TIER_TIMEOUT_MULTIPLIERS[tier.to_s] || 1.0
|
|
574
|
+
(base_timeout * multiplier).to_i
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
# Resolve a model name using the RubyLLM registry
|
|
578
|
+
#
|
|
579
|
+
# Attempts to resolve a model family name (e.g., "claude-3-5-haiku") to a
|
|
580
|
+
# versioned model name (e.g., "claude-3-5-haiku-20241022") using the RubyLLM
|
|
581
|
+
# registry. Falls back to using the name as-is if resolution fails.
|
|
582
|
+
#
|
|
583
|
+
# @param model_name [String] The model family name or versioned name
|
|
584
|
+
# @return [String] The resolved model name (versioned if found, original if not)
|
|
585
|
+
def resolve_model_name(model_name)
|
|
586
|
+
require_relative "../harness/ruby_llm_registry" unless defined?(Aidp::Harness::RubyLLMRegistry)
|
|
587
|
+
|
|
588
|
+
begin
|
|
589
|
+
registry = Aidp::Harness::RubyLLMRegistry.new
|
|
590
|
+
resolved = registry.resolve_model(model_name, provider: name)
|
|
591
|
+
|
|
592
|
+
if resolved
|
|
593
|
+
Aidp.log_debug(name, "Resolved model using registry",
|
|
594
|
+
requested: model_name,
|
|
595
|
+
resolved: resolved)
|
|
596
|
+
resolved
|
|
536
597
|
else
|
|
537
|
-
|
|
598
|
+
# Fall back to using the name as-is
|
|
599
|
+
Aidp.log_warn(name, "Model not found in registry, using as-is",
|
|
600
|
+
model: model_name)
|
|
601
|
+
model_name
|
|
538
602
|
end
|
|
603
|
+
rescue => e
|
|
604
|
+
# If registry fails, fall back to using the name as-is
|
|
605
|
+
Aidp.log_error(name, "Registry lookup failed, using model name as-is",
|
|
606
|
+
model: model_name,
|
|
607
|
+
error: e.message)
|
|
608
|
+
model_name
|
|
539
609
|
end
|
|
540
610
|
end
|
|
541
611
|
|
data/lib/aidp/providers/codex.rb
CHANGED
|
@@ -76,7 +76,7 @@ module Aidp
|
|
|
76
76
|
end
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
-
def send_message(prompt:, session: nil)
|
|
79
|
+
def send_message(prompt:, session: nil, options: {})
|
|
80
80
|
raise "codex CLI not available" unless self.class.available?
|
|
81
81
|
|
|
82
82
|
# Smart timeout calculation (store prompt length for adaptive logic)
|
|
@@ -117,7 +117,7 @@ module Aidp
|
|
|
117
117
|
fetch_mcp_servers_cli || fetch_mcp_servers_config
|
|
118
118
|
end
|
|
119
119
|
|
|
120
|
-
def send_message(prompt:, session: nil)
|
|
120
|
+
def send_message(prompt:, session: nil, options: {})
|
|
121
121
|
raise "cursor-agent not available" unless self.class.available?
|
|
122
122
|
|
|
123
123
|
# Smart timeout calculation
|