aidp 0.23.0 → 0.24.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.rb +3 -0
- data/lib/aidp/execute/work_loop_runner.rb +252 -45
- data/lib/aidp/execute/work_loop_unit_scheduler.rb +27 -2
- data/lib/aidp/harness/condition_detector.rb +42 -8
- data/lib/aidp/harness/config_manager.rb +7 -0
- data/lib/aidp/harness/config_schema.rb +25 -0
- data/lib/aidp/harness/configuration.rb +69 -6
- data/lib/aidp/harness/error_handler.rb +117 -44
- data/lib/aidp/harness/provider_manager.rb +64 -0
- data/lib/aidp/harness/provider_metrics.rb +138 -0
- data/lib/aidp/harness/runner.rb +90 -29
- data/lib/aidp/harness/simple_user_interface.rb +4 -0
- data/lib/aidp/harness/state/ui_state.rb +0 -10
- data/lib/aidp/harness/state_manager.rb +1 -15
- data/lib/aidp/harness/test_runner.rb +39 -2
- data/lib/aidp/logger.rb +34 -4
- data/lib/aidp/providers/adapter.rb +241 -0
- data/lib/aidp/providers/anthropic.rb +75 -7
- data/lib/aidp/providers/base.rb +29 -1
- data/lib/aidp/providers/capability_registry.rb +205 -0
- data/lib/aidp/providers/codex.rb +14 -0
- data/lib/aidp/providers/error_taxonomy.rb +195 -0
- data/lib/aidp/providers/gemini.rb +3 -2
- data/lib/aidp/setup/provider_registry.rb +107 -0
- data/lib/aidp/setup/wizard.rb +115 -31
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +263 -23
- data/lib/aidp/watch/repository_client.rb +4 -4
- data/lib/aidp/watch/runner.rb +37 -5
- data/lib/aidp/workflows/guided_agent.rb +53 -0
- data/lib/aidp/worktree.rb +67 -10
- data/templates/work_loop/decide_whats_next.md +21 -0
- data/templates/work_loop/diagnose_failures.md +21 -0
- metadata +10 -3
- /data/{bin → exe}/aidp +0 -0
data/lib/aidp/harness/runner.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "timeout"
|
|
4
4
|
require "json"
|
|
5
|
-
require_relative "
|
|
5
|
+
require_relative "configuration"
|
|
6
6
|
require_relative "state_manager"
|
|
7
7
|
require_relative "condition_detector"
|
|
8
8
|
require_relative "provider_manager"
|
|
@@ -44,27 +44,31 @@ module Aidp
|
|
|
44
44
|
@current_provider = nil
|
|
45
45
|
@user_input = options[:user_input] || {} # Include user input from workflow selection
|
|
46
46
|
@execution_log = []
|
|
47
|
+
@last_error = nil
|
|
47
48
|
@prompt = options[:prompt] || TTY::Prompt.new
|
|
48
49
|
|
|
49
50
|
# Store workflow configuration
|
|
50
51
|
@selected_steps = options[:selected_steps]
|
|
51
52
|
@workflow_type = options[:workflow_type]
|
|
53
|
+
@non_interactive = options[:non_interactive] || (@workflow_type == :watch_mode)
|
|
52
54
|
|
|
53
55
|
# Initialize components
|
|
54
|
-
@
|
|
56
|
+
@configuration = Configuration.new(project_dir)
|
|
55
57
|
@state_manager = StateManager.new(project_dir, @mode)
|
|
56
|
-
@provider_manager = ProviderManager.new(@
|
|
58
|
+
@provider_manager = ProviderManager.new(@configuration, prompt: @prompt)
|
|
57
59
|
|
|
58
60
|
# Use ZFC-enabled condition detector
|
|
59
61
|
# ZfcConditionDetector will create its own ProviderFactory if needed
|
|
60
62
|
# Falls back to legacy pattern matching when ZFC is disabled
|
|
61
63
|
require_relative "zfc_condition_detector"
|
|
62
|
-
@condition_detector = ZfcConditionDetector.new(@
|
|
64
|
+
@condition_detector = ZfcConditionDetector.new(@configuration)
|
|
63
65
|
|
|
64
66
|
@user_interface = SimpleUserInterface.new
|
|
65
|
-
@error_handler = ErrorHandler.new(@provider_manager, @
|
|
67
|
+
@error_handler = ErrorHandler.new(@provider_manager, @configuration)
|
|
66
68
|
@status_display = StatusDisplay.new
|
|
67
69
|
@completion_checker = CompletionChecker.new(@project_dir, @workflow_type)
|
|
70
|
+
@failure_reason = nil
|
|
71
|
+
@failure_metadata = nil
|
|
68
72
|
end
|
|
69
73
|
|
|
70
74
|
# Main execution method - runs the harness loop
|
|
@@ -114,27 +118,55 @@ module Aidp
|
|
|
114
118
|
display_message(completion_status[:summary], type: :info)
|
|
115
119
|
|
|
116
120
|
# Ask user if they want to continue anyway
|
|
117
|
-
if
|
|
118
|
-
@
|
|
119
|
-
|
|
121
|
+
if confirmation_prompt_allowed?
|
|
122
|
+
if @user_interface.get_confirmation("Continue anyway? This may indicate issues that should be addressed.", default: false)
|
|
123
|
+
@state = STATES[:completed]
|
|
124
|
+
log_execution("Harness completed with user override")
|
|
125
|
+
else
|
|
126
|
+
mark_completion_failure(completion_status)
|
|
127
|
+
@state = STATES[:error]
|
|
128
|
+
log_execution("Harness stopped due to unmet completion criteria")
|
|
129
|
+
end
|
|
120
130
|
else
|
|
131
|
+
display_message("⚠️ Non-interactive mode: cannot override failed completion criteria. Stopping run.", type: :warning)
|
|
132
|
+
mark_completion_failure(completion_status)
|
|
121
133
|
@state = STATES[:error]
|
|
122
|
-
log_execution("Harness stopped due to unmet completion criteria")
|
|
134
|
+
log_execution("Harness stopped due to unmet completion criteria in non-interactive mode")
|
|
123
135
|
end
|
|
124
136
|
end
|
|
125
137
|
end
|
|
126
138
|
rescue => e
|
|
127
139
|
@state = STATES[:error]
|
|
128
|
-
|
|
140
|
+
@last_error = e
|
|
141
|
+
log_execution("Harness error: #{e.message}", {error: e.class.name, backtrace: e.backtrace&.first(5)})
|
|
129
142
|
handle_error(e)
|
|
130
143
|
ensure
|
|
131
|
-
# Save state before exiting
|
|
132
|
-
|
|
133
|
-
|
|
144
|
+
# Save state before exiting - protect against exceptions during cleanup
|
|
145
|
+
begin
|
|
146
|
+
save_state
|
|
147
|
+
rescue => e
|
|
148
|
+
# Don't let state save failures kill the whole run or prevent cleanup
|
|
149
|
+
Aidp.logger.error("harness", "Failed to save state during cleanup: #{e.message}", error: e.class.name)
|
|
150
|
+
@last_error ||= e # Only set if no previous error
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
begin
|
|
154
|
+
cleanup
|
|
155
|
+
rescue => e
|
|
156
|
+
# Don't let cleanup failures propagate
|
|
157
|
+
Aidp.logger.error("harness", "Failed during cleanup: #{e.message}", error: e.class.name)
|
|
158
|
+
end
|
|
134
159
|
end
|
|
135
160
|
|
|
136
161
|
result = {status: @state, message: get_completion_message}
|
|
162
|
+
result[:reason] = @failure_reason if @failure_reason
|
|
163
|
+
result[:failure_metadata] = @failure_metadata if @failure_metadata
|
|
137
164
|
result[:clarification_questions] = @clarification_questions if @clarification_questions
|
|
165
|
+
if @last_error
|
|
166
|
+
result[:error] = @last_error.message
|
|
167
|
+
result[:error_class] = @last_error.class.name
|
|
168
|
+
result[:backtrace] = @last_error.backtrace&.first(10)
|
|
169
|
+
end
|
|
138
170
|
result
|
|
139
171
|
end
|
|
140
172
|
|
|
@@ -183,9 +215,9 @@ module Aidp
|
|
|
183
215
|
{
|
|
184
216
|
harness: status,
|
|
185
217
|
configuration: {
|
|
186
|
-
default_provider: @
|
|
187
|
-
fallback_providers: @
|
|
188
|
-
max_retries: @
|
|
218
|
+
default_provider: @configuration.default_provider,
|
|
219
|
+
fallback_providers: @configuration.fallback_providers,
|
|
220
|
+
max_retries: @configuration.harness_config[:max_retries]
|
|
189
221
|
},
|
|
190
222
|
provider_manager: @provider_manager.status,
|
|
191
223
|
error_stats: @error_handler.error_stats
|
|
@@ -281,15 +313,30 @@ module Aidp
|
|
|
281
313
|
log_execution("User feedback collected", {responses: user_responses.keys})
|
|
282
314
|
end
|
|
283
315
|
|
|
284
|
-
def handle_rate_limit(
|
|
316
|
+
def handle_rate_limit(result)
|
|
285
317
|
@state = STATES[:waiting_for_rate_limit]
|
|
286
318
|
log_execution("Rate limit detected, switching provider")
|
|
287
319
|
|
|
320
|
+
rate_limit_info = nil
|
|
321
|
+
if @condition_detector.respond_to?(:extract_rate_limit_info)
|
|
322
|
+
rate_limit_info = @condition_detector.extract_rate_limit_info(result, @current_provider)
|
|
323
|
+
end
|
|
324
|
+
reset_time = rate_limit_info && rate_limit_info[:reset_time]
|
|
325
|
+
|
|
288
326
|
# Mark current provider as rate limited
|
|
289
|
-
@provider_manager.mark_rate_limited(@current_provider)
|
|
327
|
+
@provider_manager.mark_rate_limited(@current_provider, reset_time)
|
|
328
|
+
|
|
329
|
+
# Provider manager might already have switched upstream (e.g., during CLI execution)
|
|
330
|
+
manager_current = @provider_manager.current_provider
|
|
331
|
+
if manager_current && manager_current != @current_provider
|
|
332
|
+
@current_provider = manager_current
|
|
333
|
+
@state = STATES[:running]
|
|
334
|
+
log_execution("Provider already switched upstream", new_provider: manager_current)
|
|
335
|
+
return
|
|
336
|
+
end
|
|
290
337
|
|
|
291
|
-
# Switch to next provider
|
|
292
|
-
next_provider = @provider_manager.switch_provider
|
|
338
|
+
# Switch to next provider explicitly when still on the rate-limited provider
|
|
339
|
+
next_provider = @provider_manager.switch_provider("rate_limit", previous_provider: @current_provider)
|
|
293
340
|
@current_provider = next_provider
|
|
294
341
|
|
|
295
342
|
if next_provider
|
|
@@ -313,6 +360,10 @@ module Aidp
|
|
|
313
360
|
end
|
|
314
361
|
end
|
|
315
362
|
|
|
363
|
+
def confirmation_prompt_allowed?
|
|
364
|
+
!@non_interactive
|
|
365
|
+
end
|
|
366
|
+
|
|
316
367
|
def sleep_until_reset(reset_time)
|
|
317
368
|
while Time.now < reset_time && @state == STATES[:waiting_for_rate_limit]
|
|
318
369
|
remaining = reset_time - Time.now
|
|
@@ -374,20 +425,14 @@ module Aidp
|
|
|
374
425
|
end
|
|
375
426
|
|
|
376
427
|
def save_state
|
|
377
|
-
# Save harness-specific state
|
|
428
|
+
# Save harness-specific state (execution_log removed to prevent unbounded growth)
|
|
378
429
|
@state_manager.save_state({
|
|
379
430
|
state: @state,
|
|
380
431
|
current_step: @current_step,
|
|
381
432
|
current_provider: @current_provider,
|
|
382
433
|
user_input: @user_input,
|
|
383
|
-
execution_log: @execution_log,
|
|
384
434
|
last_saved: Time.now
|
|
385
435
|
})
|
|
386
|
-
|
|
387
|
-
# Also save execution log entries to state manager
|
|
388
|
-
@execution_log.each do |entry|
|
|
389
|
-
@state_manager.add_execution_log(entry)
|
|
390
|
-
end
|
|
391
436
|
end
|
|
392
437
|
|
|
393
438
|
def handle_error(error)
|
|
@@ -400,6 +445,7 @@ module Aidp
|
|
|
400
445
|
end
|
|
401
446
|
|
|
402
447
|
def log_execution(message, data = {})
|
|
448
|
+
# Keep in-memory log for runtime diagnostics (not persisted)
|
|
403
449
|
log_entry = {
|
|
404
450
|
timestamp: Time.now,
|
|
405
451
|
message: message,
|
|
@@ -408,7 +454,13 @@ module Aidp
|
|
|
408
454
|
}
|
|
409
455
|
@execution_log << log_entry
|
|
410
456
|
|
|
411
|
-
#
|
|
457
|
+
# Log to persistent logger instead of state file
|
|
458
|
+
Aidp.logger.info("harness_execution", message,
|
|
459
|
+
state: @state,
|
|
460
|
+
step: @current_step,
|
|
461
|
+
**data.slice(:error, :error_class, :criteria, :all_complete, :summary).compact)
|
|
462
|
+
|
|
463
|
+
# Also log to standard output in debug mode
|
|
412
464
|
puts "[#{Time.now.strftime("%H:%M:%S")}] #{message}" if ENV["AIDP_DEBUG"] == "1"
|
|
413
465
|
end
|
|
414
466
|
|
|
@@ -419,12 +471,21 @@ module Aidp
|
|
|
419
471
|
when STATES[:stopped]
|
|
420
472
|
"Harness stopped by user."
|
|
421
473
|
when STATES[:error]
|
|
422
|
-
|
|
474
|
+
if @last_error
|
|
475
|
+
"Harness encountered an error and stopped: #{@last_error.class.name}: #{@last_error.message}"
|
|
476
|
+
else
|
|
477
|
+
"Harness encountered an error and stopped."
|
|
478
|
+
end
|
|
423
479
|
else
|
|
424
480
|
"Harness finished in state: #{@state}"
|
|
425
481
|
end
|
|
426
482
|
end
|
|
427
483
|
|
|
484
|
+
def mark_completion_failure(completion_status)
|
|
485
|
+
@failure_reason = :completion_criteria
|
|
486
|
+
@failure_metadata = completion_status
|
|
487
|
+
end
|
|
488
|
+
|
|
428
489
|
private
|
|
429
490
|
end
|
|
430
491
|
end
|
|
@@ -19,16 +19,6 @@ module Aidp
|
|
|
19
19
|
update_state(user_input: current_input)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def execution_log
|
|
23
|
-
state[:execution_log] || []
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def add_execution_log(entry)
|
|
27
|
-
current_log = execution_log
|
|
28
|
-
current_log << entry
|
|
29
|
-
update_state(execution_log: current_log)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
22
|
def current_step
|
|
33
23
|
state[:current_step]
|
|
34
24
|
end
|
|
@@ -129,20 +129,6 @@ module Aidp
|
|
|
129
129
|
update_state(user_input: current_input, last_updated: Time.now)
|
|
130
130
|
end
|
|
131
131
|
|
|
132
|
-
# Get execution log
|
|
133
|
-
def execution_log
|
|
134
|
-
state = load_state
|
|
135
|
-
return [] unless state
|
|
136
|
-
state[:execution_log] || []
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
# Add to execution log
|
|
140
|
-
def add_execution_log(entry)
|
|
141
|
-
current_log = execution_log
|
|
142
|
-
current_log << entry
|
|
143
|
-
update_state(execution_log: current_log, last_updated: Time.now)
|
|
144
|
-
end
|
|
145
|
-
|
|
146
132
|
# Get provider state
|
|
147
133
|
def provider_state
|
|
148
134
|
state = load_state
|
|
@@ -615,7 +601,7 @@ module Aidp
|
|
|
615
601
|
yield
|
|
616
602
|
end
|
|
617
603
|
end
|
|
618
|
-
rescue Aidp::Concurrency::
|
|
604
|
+
rescue Aidp::Concurrency::MaxAttemptsError
|
|
619
605
|
raise "Could not acquire state lock within timeout"
|
|
620
606
|
ensure
|
|
621
607
|
# Clean up lock file
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "open3"
|
|
4
|
+
require_relative "../tooling_detector"
|
|
4
5
|
|
|
5
6
|
module Aidp
|
|
6
7
|
module Harness
|
|
@@ -15,7 +16,7 @@ module Aidp
|
|
|
15
16
|
# Run all configured tests
|
|
16
17
|
# Returns: { success: boolean, output: string, failures: array }
|
|
17
18
|
def run_tests
|
|
18
|
-
test_commands =
|
|
19
|
+
test_commands = resolved_test_commands
|
|
19
20
|
return {success: true, output: "", failures: []} if test_commands.empty?
|
|
20
21
|
|
|
21
22
|
results = test_commands.map { |cmd| execute_command(cmd, "test") }
|
|
@@ -25,7 +26,7 @@ module Aidp
|
|
|
25
26
|
# Run all configured linters
|
|
26
27
|
# Returns: { success: boolean, output: string, failures: array }
|
|
27
28
|
def run_linters
|
|
28
|
-
lint_commands =
|
|
29
|
+
lint_commands = resolved_lint_commands
|
|
29
30
|
return {success: true, output: "", failures: []} if lint_commands.empty?
|
|
30
31
|
|
|
31
32
|
results = lint_commands.map { |cmd| execute_command(cmd, "linter") }
|
|
@@ -78,6 +79,42 @@ module Aidp
|
|
|
78
79
|
|
|
79
80
|
output.join("\n")
|
|
80
81
|
end
|
|
82
|
+
|
|
83
|
+
def resolved_test_commands
|
|
84
|
+
explicit = Array(@config.test_commands).compact.map(&:strip).reject(&:empty?)
|
|
85
|
+
return explicit unless explicit.empty?
|
|
86
|
+
|
|
87
|
+
detected = detected_tooling.test_commands
|
|
88
|
+
log_fallback(:tests, detected) unless detected.empty?
|
|
89
|
+
detected
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def resolved_lint_commands
|
|
93
|
+
explicit = Array(@config.lint_commands).compact.map(&:strip).reject(&:empty?)
|
|
94
|
+
return explicit unless explicit.empty?
|
|
95
|
+
|
|
96
|
+
detected = detected_tooling.lint_commands
|
|
97
|
+
log_fallback(:linters, detected) unless detected.empty?
|
|
98
|
+
detected
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def detected_tooling
|
|
102
|
+
@detected_tooling ||= Aidp::ToolingDetector.detect(@project_dir)
|
|
103
|
+
rescue => e
|
|
104
|
+
Aidp.log_warn("test_runner", "tooling_detection_failed", error: e.message)
|
|
105
|
+
Aidp::ToolingDetector::Result.new(test_commands: [], lint_commands: [])
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def log_fallback(type, commands)
|
|
109
|
+
Aidp.log_info(
|
|
110
|
+
"test_runner",
|
|
111
|
+
"auto_detected_commands",
|
|
112
|
+
category: type,
|
|
113
|
+
commands: commands
|
|
114
|
+
)
|
|
115
|
+
rescue NameError
|
|
116
|
+
# Logging infrastructure not available in some tests
|
|
117
|
+
end
|
|
81
118
|
end
|
|
82
119
|
end
|
|
83
120
|
end
|
data/lib/aidp/logger.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "logger"
|
|
4
4
|
require "json"
|
|
5
5
|
require "fileutils"
|
|
6
|
+
require "pathname"
|
|
6
7
|
|
|
7
8
|
module Aidp
|
|
8
9
|
# Unified structured logger for all AIDP operations
|
|
@@ -84,12 +85,30 @@ module Aidp
|
|
|
84
85
|
private
|
|
85
86
|
|
|
86
87
|
def determine_log_level
|
|
87
|
-
# Priority:
|
|
88
|
-
level_str = ENV["AIDP_LOG_LEVEL"]
|
|
89
|
-
|
|
88
|
+
# Priority: explicit env override > DEBUG flags > config > default
|
|
89
|
+
level_str = if ENV["AIDP_LOG_LEVEL"]
|
|
90
|
+
ENV["AIDP_LOG_LEVEL"]
|
|
91
|
+
elsif debug_env_enabled?
|
|
92
|
+
"debug"
|
|
93
|
+
elsif @config[:level]
|
|
94
|
+
@config[:level]
|
|
95
|
+
else
|
|
96
|
+
"info"
|
|
97
|
+
end
|
|
98
|
+
level_sym = level_str.to_s.to_sym
|
|
90
99
|
LEVELS.key?(level_sym) ? level_sym : :info
|
|
91
100
|
end
|
|
92
101
|
|
|
102
|
+
def debug_env_enabled?
|
|
103
|
+
raw = ENV["AIDP_DEBUG"] || ENV["DEBUG"]
|
|
104
|
+
return false if raw.nil?
|
|
105
|
+
|
|
106
|
+
normalized = raw.to_s.strip.downcase
|
|
107
|
+
return true if %w[true on yes debug].include?(normalized)
|
|
108
|
+
|
|
109
|
+
/\A\d+\z/.match?(normalized) ? normalized.to_i.positive? : false
|
|
110
|
+
end
|
|
111
|
+
|
|
93
112
|
def should_log?(level)
|
|
94
113
|
LEVELS[level] >= LEVELS[@level]
|
|
95
114
|
end
|
|
@@ -106,7 +125,7 @@ module Aidp
|
|
|
106
125
|
end
|
|
107
126
|
|
|
108
127
|
def setup_logger
|
|
109
|
-
info_path =
|
|
128
|
+
info_path = determine_log_file_path
|
|
110
129
|
@logger = create_logger(info_path)
|
|
111
130
|
# Emit instrumentation after logger is available (avoid recursive Aidp.log_* calls during bootstrap)
|
|
112
131
|
return unless @instrument_internal
|
|
@@ -181,6 +200,17 @@ module Aidp
|
|
|
181
200
|
JSON.generate(entry)
|
|
182
201
|
end
|
|
183
202
|
|
|
203
|
+
def determine_log_file_path
|
|
204
|
+
custom = (ENV["AIDP_LOG_FILE"] || @config[:file]).to_s.strip
|
|
205
|
+
relative_path = custom.empty? ? INFO_LOG : custom
|
|
206
|
+
|
|
207
|
+
if Pathname.new(relative_path).absolute?
|
|
208
|
+
relative_path
|
|
209
|
+
else
|
|
210
|
+
File.join(@project_dir, relative_path)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
184
214
|
# Redaction patterns for common secrets
|
|
185
215
|
REDACTION_PATTERNS = [
|
|
186
216
|
# API keys and tokens (with capture groups)
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aidp
|
|
4
|
+
module Providers
|
|
5
|
+
# ProviderAdapter defines the standardized interface that all provider implementations
|
|
6
|
+
# must conform to. This ensures consistent behavior across different AI model providers
|
|
7
|
+
# while allowing for provider-specific implementations.
|
|
8
|
+
#
|
|
9
|
+
# Design Philosophy:
|
|
10
|
+
# - Adapters are stateless; delegate throttling, retries, and escalation to coordinator
|
|
11
|
+
# - Store provider-specific regex matchers adjacent to adapters for maintainability
|
|
12
|
+
# - Single semantic flags map to provider-specific equivalents
|
|
13
|
+
#
|
|
14
|
+
# @see https://github.com/viamin/aidp/issues/243
|
|
15
|
+
module Adapter
|
|
16
|
+
# Core interface methods that all providers must implement
|
|
17
|
+
|
|
18
|
+
# Provider identifier (e.g., "anthropic", "cursor", "gemini")
|
|
19
|
+
# @return [String] unique lowercase identifier for this provider
|
|
20
|
+
def name
|
|
21
|
+
raise NotImplementedError, "#{self.class} must implement #name"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Human-friendly display name for UI
|
|
25
|
+
# @return [String] display name (e.g., "Anthropic Claude", "Cursor AI")
|
|
26
|
+
def display_name
|
|
27
|
+
name
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Send a message to the provider and get a response
|
|
31
|
+
# @param prompt [String] the prompt to send
|
|
32
|
+
# @param session [String, nil] optional session identifier for context
|
|
33
|
+
# @param options [Hash] additional options for the request
|
|
34
|
+
# @return [Hash, String] provider response
|
|
35
|
+
def send_message(prompt:, session: nil, **options)
|
|
36
|
+
raise NotImplementedError, "#{self.class} must implement #send_message"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Capability declaration methods
|
|
40
|
+
|
|
41
|
+
# Check if the provider supports Model Context Protocol
|
|
42
|
+
# @return [Boolean] true if MCP is supported
|
|
43
|
+
def supports_mcp?
|
|
44
|
+
false
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Fetch MCP servers configured for this provider
|
|
48
|
+
# @return [Array<Hash>] array of MCP server configurations
|
|
49
|
+
def fetch_mcp_servers
|
|
50
|
+
[]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Check if the provider is available on this system
|
|
54
|
+
# @return [Boolean] true if provider CLI or API is accessible
|
|
55
|
+
def available?
|
|
56
|
+
true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Declare provider capabilities
|
|
60
|
+
# @return [Hash] capabilities hash with feature flags
|
|
61
|
+
# @example
|
|
62
|
+
# {
|
|
63
|
+
# reasoning_tiers: ["mini", "standard", "thinking"],
|
|
64
|
+
# context_window: 200_000,
|
|
65
|
+
# supports_json_mode: true,
|
|
66
|
+
# supports_tool_use: true,
|
|
67
|
+
# supports_vision: false,
|
|
68
|
+
# supports_file_upload: true,
|
|
69
|
+
# streaming: true
|
|
70
|
+
# }
|
|
71
|
+
def capabilities
|
|
72
|
+
{
|
|
73
|
+
reasoning_tiers: [],
|
|
74
|
+
context_window: 100_000,
|
|
75
|
+
supports_json_mode: false,
|
|
76
|
+
supports_tool_use: false,
|
|
77
|
+
supports_vision: false,
|
|
78
|
+
supports_file_upload: false,
|
|
79
|
+
streaming: false
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Dangerous permissions abstraction
|
|
84
|
+
|
|
85
|
+
# Check if the provider supports dangerous/elevated permissions mode
|
|
86
|
+
# @return [Boolean] true if dangerous mode is supported
|
|
87
|
+
def supports_dangerous_mode?
|
|
88
|
+
false
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Get the provider-specific flag(s) for enabling dangerous mode
|
|
92
|
+
# Maps the semantic `dangerous: true` flag to provider-specific equivalents
|
|
93
|
+
# @return [Array<String>] provider-specific CLI flags
|
|
94
|
+
# @example Anthropic
|
|
95
|
+
# ["--dangerously-skip-permissions"]
|
|
96
|
+
# @example Gemini (hypothetical)
|
|
97
|
+
# ["--yolo"]
|
|
98
|
+
def dangerous_mode_flags
|
|
99
|
+
[]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Check if dangerous mode is currently enabled
|
|
103
|
+
# @return [Boolean] true if dangerous mode is active
|
|
104
|
+
def dangerous_mode_enabled?
|
|
105
|
+
@dangerous_mode_enabled ||= false
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Enable or disable dangerous mode
|
|
109
|
+
# @param enabled [Boolean] whether to enable dangerous mode
|
|
110
|
+
# @return [void]
|
|
111
|
+
def dangerous_mode=(enabled)
|
|
112
|
+
@dangerous_mode_enabled = enabled
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Error classification and handling
|
|
116
|
+
|
|
117
|
+
# Get error classification regex patterns for this provider
|
|
118
|
+
# @return [Hash<Symbol, Array<Regexp>>] mapping of error categories to regex patterns
|
|
119
|
+
# @example
|
|
120
|
+
# {
|
|
121
|
+
# rate_limited: [/rate.?limit/i, /quota.*exceeded/i],
|
|
122
|
+
# auth_expired: [/authentication.*failed/i, /invalid.*api.*key/i],
|
|
123
|
+
# quota_exceeded: [/quota.*exceeded/i, /usage.*limit/i],
|
|
124
|
+
# transient: [/timeout/i, /connection.*reset/i, /temporary.*error/i],
|
|
125
|
+
# permanent: [/invalid.*model/i, /unsupported.*operation/i]
|
|
126
|
+
# }
|
|
127
|
+
def error_patterns
|
|
128
|
+
{}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Classify an error into the standardized error taxonomy
|
|
132
|
+
# @param error [StandardError] the error to classify
|
|
133
|
+
# @return [Symbol] error category (:rate_limited, :auth_expired, :quota_exceeded, :transient, :permanent)
|
|
134
|
+
def classify_error(error)
|
|
135
|
+
message = error.message.to_s
|
|
136
|
+
|
|
137
|
+
# First check provider-specific patterns
|
|
138
|
+
error_patterns.each do |category, patterns|
|
|
139
|
+
patterns.each do |pattern|
|
|
140
|
+
return category if message.match?(pattern)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Fall back to ErrorTaxonomy for classification
|
|
145
|
+
require_relative "error_taxonomy"
|
|
146
|
+
Aidp::Providers::ErrorTaxonomy.classify_message(message)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Get normalized error metadata
|
|
150
|
+
# @param error [StandardError] the error to process
|
|
151
|
+
# @return [Hash] normalized error information
|
|
152
|
+
def error_metadata(error)
|
|
153
|
+
{
|
|
154
|
+
provider: name,
|
|
155
|
+
error_category: classify_error(error),
|
|
156
|
+
error_class: error.class.name,
|
|
157
|
+
message: redact_secrets(error.message),
|
|
158
|
+
timestamp: Time.now.iso8601,
|
|
159
|
+
retryable: retryable_error?(error)
|
|
160
|
+
}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Check if an error is retryable
|
|
164
|
+
# @param error [StandardError] the error to check
|
|
165
|
+
# @return [Boolean] true if the error should be retried
|
|
166
|
+
def retryable_error?(error)
|
|
167
|
+
category = classify_error(error)
|
|
168
|
+
[:transient].include?(category)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Logging and metrics
|
|
172
|
+
|
|
173
|
+
# Get logging metadata for this provider
|
|
174
|
+
# @return [Hash] metadata for structured logging
|
|
175
|
+
def logging_metadata
|
|
176
|
+
{
|
|
177
|
+
provider: name,
|
|
178
|
+
display_name: display_name,
|
|
179
|
+
supports_mcp: supports_mcp?,
|
|
180
|
+
available: available?,
|
|
181
|
+
dangerous_mode: dangerous_mode_enabled?
|
|
182
|
+
}
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Redact secrets from log messages
|
|
186
|
+
# @param message [String] message potentially containing secrets
|
|
187
|
+
# @return [String] message with secrets redacted
|
|
188
|
+
def redact_secrets(message)
|
|
189
|
+
# Redact common secret patterns
|
|
190
|
+
message = message.gsub(/api[_-]?key[:\s=]+[^\s&]+/i, "api_key=[REDACTED]")
|
|
191
|
+
message = message.gsub(/token[:\s=]+[^\s&]+/i, "token=[REDACTED]")
|
|
192
|
+
message = message.gsub(/password[:\s=]+[^\s&]+/i, "password=[REDACTED]")
|
|
193
|
+
message = message.gsub(/bearer\s+[^\s&]+/i, "bearer [REDACTED]")
|
|
194
|
+
message.gsub(/sk-[a-zA-Z0-9_-]{20,}/i, "sk-[REDACTED]")
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Configuration validation
|
|
198
|
+
|
|
199
|
+
# Validate provider configuration
|
|
200
|
+
# @param config [Hash] configuration to validate
|
|
201
|
+
# @return [Hash] validation result with :valid, :errors, :warnings keys
|
|
202
|
+
def validate_config(config)
|
|
203
|
+
errors = []
|
|
204
|
+
warnings = []
|
|
205
|
+
|
|
206
|
+
# Validate required fields
|
|
207
|
+
unless config[:type]
|
|
208
|
+
errors << "Provider type is required"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
unless ["usage_based", "subscription", "passthrough"].include?(config[:type])
|
|
212
|
+
errors << "Provider type must be one of: usage_based, subscription, passthrough"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Validate models if present
|
|
216
|
+
if config[:models] && !config[:models].is_a?(Array)
|
|
217
|
+
errors << "Models must be an array"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
{
|
|
221
|
+
valid: errors.empty?,
|
|
222
|
+
errors: errors,
|
|
223
|
+
warnings: warnings
|
|
224
|
+
}
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Provider health and status
|
|
228
|
+
|
|
229
|
+
# Check provider health
|
|
230
|
+
# @return [Hash] health status information
|
|
231
|
+
def health_status
|
|
232
|
+
{
|
|
233
|
+
provider: name,
|
|
234
|
+
available: available?,
|
|
235
|
+
healthy: available?,
|
|
236
|
+
timestamp: Time.now.iso8601
|
|
237
|
+
}
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|