aidp 0.14.1 → 0.15.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 +46 -45
- data/lib/aidp/cli.rb +46 -38
- data/lib/aidp/debug_mixin.rb +34 -33
- data/lib/aidp/execute/agent_signal_parser.rb +46 -0
- data/lib/aidp/execute/async_work_loop_runner.rb +5 -0
- data/lib/aidp/execute/checkpoint.rb +28 -5
- data/lib/aidp/execute/deterministic_unit.rb +254 -0
- data/lib/aidp/execute/interactive_repl.rb +7 -0
- data/lib/aidp/execute/steps.rb +1 -1
- data/lib/aidp/execute/work_loop_runner.rb +187 -30
- data/lib/aidp/execute/work_loop_unit_scheduler.rb +190 -0
- data/lib/aidp/harness/config_schema.rb +91 -1
- data/lib/aidp/harness/configuration.rb +60 -1
- data/lib/aidp/harness/enhanced_runner.rb +2 -0
- data/lib/aidp/harness/provider_info.rb +14 -4
- data/lib/aidp/harness/provider_manager.rb +64 -12
- data/lib/aidp/jobs/background_runner.rb +10 -3
- data/lib/aidp/logger.rb +10 -71
- data/lib/aidp/providers/base.rb +2 -0
- data/lib/aidp/providers/github_copilot.rb +12 -0
- data/lib/aidp/rescue_logging.rb +36 -0
- data/lib/aidp/setup/wizard.rb +42 -46
- data/lib/aidp/storage/csv_storage.rb +33 -7
- data/lib/aidp/storage/json_storage.rb +33 -10
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/workflows/guided_agent.rb +95 -22
- data/templates/implementation/simple_task.md +5 -0
- metadata +5 -2
- data/lib/aidp/debug_logger.rb +0 -195
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "deterministic_unit"
|
|
4
|
+
require_relative "../logger"
|
|
5
|
+
|
|
6
|
+
module Aidp
|
|
7
|
+
module Execute
|
|
8
|
+
class WorkLoopUnitScheduler
|
|
9
|
+
Unit = Struct.new(:type, :name, :definition, keyword_init: true) do
|
|
10
|
+
def agentic?
|
|
11
|
+
type == :agentic
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def deterministic?
|
|
15
|
+
type == :deterministic
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
attr_reader :last_agentic_summary
|
|
20
|
+
|
|
21
|
+
def initialize(units_config, clock: Time)
|
|
22
|
+
@clock = clock
|
|
23
|
+
@deterministic_definitions = build_deterministic_definitions(units_config[:deterministic])
|
|
24
|
+
@defaults = default_options.merge(units_config[:defaults] || {})
|
|
25
|
+
@pending_units = []
|
|
26
|
+
@deterministic_history = []
|
|
27
|
+
@deterministic_state = Hash.new { |h, key| h[key] = default_deterministic_state }
|
|
28
|
+
@agentic_runs = []
|
|
29
|
+
@last_agentic_summary = nil
|
|
30
|
+
@consecutive_deciders = 0
|
|
31
|
+
@completed = false
|
|
32
|
+
@started = false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def next_unit
|
|
36
|
+
return nil if @completed
|
|
37
|
+
|
|
38
|
+
unless @started
|
|
39
|
+
@started = true
|
|
40
|
+
queue_requested_unit(@defaults[:initial_unit] || :agentic)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
unit = @pending_units.shift
|
|
44
|
+
return unit if unit
|
|
45
|
+
|
|
46
|
+
queue_requested_unit(@defaults[:on_no_next_step] || :agentic)
|
|
47
|
+
|
|
48
|
+
@pending_units.shift
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def record_agentic_result(result, requested_next: nil, summary: nil, completed: false)
|
|
52
|
+
@last_agentic_summary = summarize(summary)
|
|
53
|
+
@agentic_runs << {timestamp: @clock.now, result: result}
|
|
54
|
+
|
|
55
|
+
queue_requested_unit(requested_next) if requested_next
|
|
56
|
+
|
|
57
|
+
mark_completed if completed && !requested_next
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def record_deterministic_result(definition, result)
|
|
61
|
+
state = @deterministic_state[definition.name]
|
|
62
|
+
state[:last_run_at] = result.finished_at
|
|
63
|
+
|
|
64
|
+
state[:current_backoff] = if result.success? || result.status == :event
|
|
65
|
+
definition.min_interval_seconds
|
|
66
|
+
else
|
|
67
|
+
[
|
|
68
|
+
state[:current_backoff] * definition.backoff_multiplier,
|
|
69
|
+
definition.max_backoff_seconds
|
|
70
|
+
].min
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
@deterministic_history << {
|
|
74
|
+
name: definition.name,
|
|
75
|
+
status: result.status,
|
|
76
|
+
output_path: result.output_path,
|
|
77
|
+
finished_at: result.finished_at,
|
|
78
|
+
data: result.data
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
requested = definition.next_for(result.status)
|
|
82
|
+
queue_requested_unit(requested) if requested
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def deterministic_context(limit: 5)
|
|
86
|
+
@deterministic_history.last(limit)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def completed?
|
|
90
|
+
@completed
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def summarize(summary)
|
|
96
|
+
return nil unless summary
|
|
97
|
+
content = summary.to_s.strip
|
|
98
|
+
return nil if content.empty?
|
|
99
|
+
(content.length > 500) ? "#{content[0...500]}…" : content
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def mark_completed
|
|
103
|
+
@completed = true
|
|
104
|
+
@pending_units.clear
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def queue_requested_unit(identifier)
|
|
108
|
+
return if identifier.nil?
|
|
109
|
+
|
|
110
|
+
case identifier.to_sym
|
|
111
|
+
when :agentic
|
|
112
|
+
enqueue_agentic(:primary)
|
|
113
|
+
when :decide_whats_next
|
|
114
|
+
enqueue_agentic(:decide_whats_next)
|
|
115
|
+
else
|
|
116
|
+
enqueue_deterministic(identifier.to_s)
|
|
117
|
+
end
|
|
118
|
+
rescue NoMethodError
|
|
119
|
+
enqueue_agentic(:primary)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def enqueue_agentic(name)
|
|
123
|
+
if name == :decide_whats_next
|
|
124
|
+
if @consecutive_deciders >= @defaults[:max_consecutive_deciders]
|
|
125
|
+
enqueue_agentic(:primary)
|
|
126
|
+
return
|
|
127
|
+
end
|
|
128
|
+
@consecutive_deciders += 1
|
|
129
|
+
else
|
|
130
|
+
@consecutive_deciders = 0
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
@pending_units << Unit.new(type: :agentic, name: name)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def enqueue_deterministic(name)
|
|
137
|
+
definition = @deterministic_definitions[name]
|
|
138
|
+
unless definition
|
|
139
|
+
enqueue_agentic((@defaults[:fallback_agentic] || :agentic).to_sym)
|
|
140
|
+
return
|
|
141
|
+
end
|
|
142
|
+
return unless definition.enabled
|
|
143
|
+
|
|
144
|
+
state = @deterministic_state[definition.name]
|
|
145
|
+
state[:current_backoff] ||= definition.min_interval_seconds
|
|
146
|
+
|
|
147
|
+
if cooldown_remaining(definition).positive?
|
|
148
|
+
enqueue_agentic((@defaults[:fallback_agentic] || :agentic).to_sym)
|
|
149
|
+
return
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
@pending_units << Unit.new(type: :deterministic, name: definition.name, definition: definition)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def cooldown_remaining(definition)
|
|
156
|
+
state = @deterministic_state[definition.name]
|
|
157
|
+
state[:current_backoff] ||= definition.min_interval_seconds
|
|
158
|
+
|
|
159
|
+
return 0 unless state[:last_run_at]
|
|
160
|
+
|
|
161
|
+
next_allowed_at = state[:last_run_at] + state[:current_backoff]
|
|
162
|
+
remaining = next_allowed_at - @clock.now
|
|
163
|
+
remaining.positive? ? remaining : 0
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def build_deterministic_definitions(config_list)
|
|
167
|
+
Array(config_list).each_with_object({}) do |config, mapping|
|
|
168
|
+
definition = DeterministicUnits::Definition.new(config.transform_keys(&:to_sym))
|
|
169
|
+
mapping[definition.name] = definition
|
|
170
|
+
rescue KeyError, ArgumentError => e
|
|
171
|
+
Aidp.logger.warn("work_loop", "Skipping invalid deterministic unit configuration",
|
|
172
|
+
name: config[:name], error: e.message)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def default_deterministic_state
|
|
177
|
+
{last_run_at: nil, current_backoff: nil}
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def default_options
|
|
181
|
+
{
|
|
182
|
+
initial_unit: :agentic,
|
|
183
|
+
on_no_next_step: :agentic,
|
|
184
|
+
fallback_agentic: :decide_whats_next,
|
|
185
|
+
max_consecutive_deciders: 1
|
|
186
|
+
}
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -374,7 +374,8 @@ module Aidp
|
|
|
374
374
|
enabled: true,
|
|
375
375
|
max_iterations: 50,
|
|
376
376
|
test_commands: [],
|
|
377
|
-
lint_commands: []
|
|
377
|
+
lint_commands: [],
|
|
378
|
+
units: {}
|
|
378
379
|
},
|
|
379
380
|
properties: {
|
|
380
381
|
enabled: {
|
|
@@ -405,6 +406,95 @@ module Aidp
|
|
|
405
406
|
type: :string
|
|
406
407
|
}
|
|
407
408
|
},
|
|
409
|
+
units: {
|
|
410
|
+
type: :hash,
|
|
411
|
+
required: false,
|
|
412
|
+
default: {},
|
|
413
|
+
properties: {
|
|
414
|
+
deterministic: {
|
|
415
|
+
type: :array,
|
|
416
|
+
required: false,
|
|
417
|
+
default: [],
|
|
418
|
+
items: {
|
|
419
|
+
type: :hash,
|
|
420
|
+
properties: {
|
|
421
|
+
name: {
|
|
422
|
+
type: :string,
|
|
423
|
+
required: true
|
|
424
|
+
},
|
|
425
|
+
command: {
|
|
426
|
+
type: :string,
|
|
427
|
+
required: false
|
|
428
|
+
},
|
|
429
|
+
type: {
|
|
430
|
+
type: :string,
|
|
431
|
+
required: false,
|
|
432
|
+
enum: ["command", "wait"]
|
|
433
|
+
},
|
|
434
|
+
output_file: {
|
|
435
|
+
type: :string,
|
|
436
|
+
required: false
|
|
437
|
+
},
|
|
438
|
+
enabled: {
|
|
439
|
+
type: :boolean,
|
|
440
|
+
required: false,
|
|
441
|
+
default: true
|
|
442
|
+
},
|
|
443
|
+
min_interval_seconds: {
|
|
444
|
+
type: :integer,
|
|
445
|
+
required: false,
|
|
446
|
+
min: 1
|
|
447
|
+
},
|
|
448
|
+
max_backoff_seconds: {
|
|
449
|
+
type: :integer,
|
|
450
|
+
required: false,
|
|
451
|
+
min: 1
|
|
452
|
+
},
|
|
453
|
+
backoff_multiplier: {
|
|
454
|
+
type: :number,
|
|
455
|
+
required: false,
|
|
456
|
+
min: 1.0
|
|
457
|
+
},
|
|
458
|
+
metadata: {
|
|
459
|
+
type: :hash,
|
|
460
|
+
required: false,
|
|
461
|
+
default: {}
|
|
462
|
+
},
|
|
463
|
+
next: {
|
|
464
|
+
type: :hash,
|
|
465
|
+
required: false,
|
|
466
|
+
default: {}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
defaults: {
|
|
472
|
+
type: :hash,
|
|
473
|
+
required: false,
|
|
474
|
+
default: {},
|
|
475
|
+
properties: {
|
|
476
|
+
initial_unit: {
|
|
477
|
+
type: :string,
|
|
478
|
+
required: false
|
|
479
|
+
},
|
|
480
|
+
on_no_next_step: {
|
|
481
|
+
type: :string,
|
|
482
|
+
required: false
|
|
483
|
+
},
|
|
484
|
+
fallback_agentic: {
|
|
485
|
+
type: :string,
|
|
486
|
+
required: false
|
|
487
|
+
},
|
|
488
|
+
max_consecutive_deciders: {
|
|
489
|
+
type: :integer,
|
|
490
|
+
required: false,
|
|
491
|
+
min: 1,
|
|
492
|
+
max: 10
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
},
|
|
408
498
|
guards: {
|
|
409
499
|
type: :hash,
|
|
410
500
|
required: false,
|
|
@@ -150,6 +150,10 @@ module Aidp
|
|
|
150
150
|
harness_config[:work_loop] || default_work_loop_config
|
|
151
151
|
end
|
|
152
152
|
|
|
153
|
+
def work_loop_units_config
|
|
154
|
+
work_loop_config[:units] || default_units_config
|
|
155
|
+
end
|
|
156
|
+
|
|
153
157
|
# Check if work loops are enabled
|
|
154
158
|
def work_loop_enabled?
|
|
155
159
|
work_loop_config[:enabled]
|
|
@@ -483,7 +487,62 @@ module Aidp
|
|
|
483
487
|
max_iterations: 50,
|
|
484
488
|
test_commands: [],
|
|
485
489
|
lint_commands: [],
|
|
486
|
-
guards: default_guards_config
|
|
490
|
+
guards: default_guards_config,
|
|
491
|
+
units: default_units_config
|
|
492
|
+
}
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def default_units_config
|
|
496
|
+
{
|
|
497
|
+
deterministic: [
|
|
498
|
+
{
|
|
499
|
+
name: "run_full_tests",
|
|
500
|
+
command: "bundle exec rake spec",
|
|
501
|
+
output_file: ".aidp/out/run_full_tests.yml",
|
|
502
|
+
enabled: false,
|
|
503
|
+
min_interval_seconds: 300,
|
|
504
|
+
max_backoff_seconds: 1800,
|
|
505
|
+
next: {
|
|
506
|
+
success: :agentic,
|
|
507
|
+
failure: :decide_whats_next,
|
|
508
|
+
else: :decide_whats_next
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
name: "run_lint",
|
|
513
|
+
command: "bundle exec standardrb",
|
|
514
|
+
output_file: ".aidp/out/run_lint.yml",
|
|
515
|
+
enabled: false,
|
|
516
|
+
min_interval_seconds: 300,
|
|
517
|
+
max_backoff_seconds: 1800,
|
|
518
|
+
next: {
|
|
519
|
+
success: :agentic,
|
|
520
|
+
failure: :decide_whats_next,
|
|
521
|
+
else: :decide_whats_next
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
name: "wait_for_github",
|
|
526
|
+
type: :wait,
|
|
527
|
+
output_file: ".aidp/out/wait_for_github.yml",
|
|
528
|
+
metadata: {
|
|
529
|
+
interval_seconds: 60,
|
|
530
|
+
backoff_seconds: 60
|
|
531
|
+
},
|
|
532
|
+
min_interval_seconds: 60,
|
|
533
|
+
max_backoff_seconds: 900,
|
|
534
|
+
next: {
|
|
535
|
+
event: :agentic,
|
|
536
|
+
else: :wait_for_github
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
],
|
|
540
|
+
defaults: {
|
|
541
|
+
initial_unit: :agentic,
|
|
542
|
+
on_no_next_step: :wait_for_github,
|
|
543
|
+
fallback_agentic: :decide_whats_next,
|
|
544
|
+
max_consecutive_deciders: 1
|
|
545
|
+
}
|
|
487
546
|
}
|
|
488
547
|
end
|
|
489
548
|
|
|
@@ -81,6 +81,8 @@ module Aidp
|
|
|
81
81
|
@state = STATES[:running]
|
|
82
82
|
@start_time = Time.now
|
|
83
83
|
|
|
84
|
+
Aidp.logger.info("harness_runner", "Starting harness execution", mode: @mode, workflow_type: @workflow_type, steps_count: @selected_steps.size)
|
|
85
|
+
|
|
84
86
|
@tui.show_message("🚀 Starting #{@mode.to_s.capitalize} Mode", :info)
|
|
85
87
|
|
|
86
88
|
begin
|
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "yaml"
|
|
5
5
|
require "fileutils"
|
|
6
|
+
require_relative "../rescue_logging"
|
|
6
7
|
|
|
7
8
|
module Aidp
|
|
8
9
|
module Harness
|
|
9
10
|
# Stores detailed information about AI providers gathered from their CLI tools
|
|
10
11
|
class ProviderInfo
|
|
12
|
+
include Aidp::RescueLogging
|
|
13
|
+
|
|
11
14
|
attr_reader :provider_name, :info_file_path
|
|
12
15
|
|
|
13
16
|
def initialize(provider_name, root_dir = nil)
|
|
@@ -56,6 +59,7 @@ module Aidp
|
|
|
56
59
|
|
|
57
60
|
YAML.safe_load_file(@info_file_path, permitted_classes: [Time, Symbol])
|
|
58
61
|
rescue => e
|
|
62
|
+
log_rescue(e, component: "provider_info", action: "load_yaml", fallback: nil, provider: @provider_name, path: @info_file_path)
|
|
59
63
|
warn "Failed to load provider info for #{@provider_name}: #{e.message}"
|
|
60
64
|
nil
|
|
61
65
|
end
|
|
@@ -133,7 +137,8 @@ module Aidp
|
|
|
133
137
|
|
|
134
138
|
last_checked = Time.parse(info[:last_checked].to_s)
|
|
135
139
|
(Time.now - last_checked) > max_age
|
|
136
|
-
rescue
|
|
140
|
+
rescue => e
|
|
141
|
+
log_rescue(e, component: "provider_info", action: "parse_last_checked_time", fallback: true, provider: @provider_name, timestamp: info[:last_checked])
|
|
137
142
|
true
|
|
138
143
|
end
|
|
139
144
|
|
|
@@ -147,6 +152,7 @@ module Aidp
|
|
|
147
152
|
|
|
148
153
|
provider_instance.fetch_mcp_servers
|
|
149
154
|
rescue => e
|
|
155
|
+
log_rescue(e, component: "provider_info", action: "fetch_mcp_servers", fallback: [], provider: @provider_name)
|
|
150
156
|
warn "Failed to fetch MCP servers for #{@provider_name}: #{e.message}" if ENV["AIDP_DEBUG"]
|
|
151
157
|
[]
|
|
152
158
|
end
|
|
@@ -157,7 +163,8 @@ module Aidp
|
|
|
157
163
|
# Try to find the binary
|
|
158
164
|
path = begin
|
|
159
165
|
Aidp::Util.which(binary_name)
|
|
160
|
-
rescue
|
|
166
|
+
rescue => e
|
|
167
|
+
log_rescue(e, component: "provider_info", action: "locate_provider_binary", fallback: nil, provider: @provider_name, binary: binary_name)
|
|
161
168
|
nil
|
|
162
169
|
end
|
|
163
170
|
return nil unless path
|
|
@@ -183,7 +190,8 @@ module Aidp
|
|
|
183
190
|
Process.kill("TERM", pid)
|
|
184
191
|
sleep 0.1
|
|
185
192
|
Process.kill("KILL", pid)
|
|
186
|
-
rescue
|
|
193
|
+
rescue => e
|
|
194
|
+
log_rescue(e, component: "provider_info", action: "kill_timeout_provider_command", fallback: nil, provider: @provider_name, binary: binary_name, pid: pid)
|
|
187
195
|
nil
|
|
188
196
|
end
|
|
189
197
|
return nil
|
|
@@ -192,7 +200,8 @@ module Aidp
|
|
|
192
200
|
output = r.read
|
|
193
201
|
r.close
|
|
194
202
|
output
|
|
195
|
-
rescue
|
|
203
|
+
rescue => e
|
|
204
|
+
log_rescue(e, component: "provider_info", action: "execute_provider_command", fallback: nil, provider: @provider_name, binary: binary_name, args: args)
|
|
196
205
|
nil
|
|
197
206
|
end
|
|
198
207
|
end
|
|
@@ -355,6 +364,7 @@ module Aidp
|
|
|
355
364
|
# Create provider instance
|
|
356
365
|
@provider_instance = provider_class.new
|
|
357
366
|
rescue => e
|
|
367
|
+
log_rescue(e, component: "provider_info", action: "create_provider_instance", fallback: nil, provider: @provider_name)
|
|
358
368
|
warn "Failed to create provider instance for #{@provider_name}: #{e.message}" if ENV["AIDP_DEBUG"]
|
|
359
369
|
nil
|
|
360
370
|
end
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
require "tty-prompt"
|
|
4
4
|
require_relative "provider_factory"
|
|
5
|
+
require_relative "../rescue_logging"
|
|
5
6
|
|
|
6
7
|
module Aidp
|
|
7
8
|
module Harness
|
|
8
9
|
# Manages provider switching and fallback logic
|
|
9
10
|
class ProviderManager
|
|
10
11
|
include Aidp::MessageDisplay
|
|
12
|
+
include Aidp::RescueLogging
|
|
11
13
|
|
|
12
14
|
def initialize(configuration, prompt: TTY::Prompt.new)
|
|
13
15
|
@configuration = configuration
|
|
@@ -69,6 +71,8 @@ module Aidp
|
|
|
69
71
|
|
|
70
72
|
# Switch to next available provider with sophisticated fallback logic
|
|
71
73
|
def switch_provider(reason = "manual_switch", context = {})
|
|
74
|
+
Aidp.logger.info("provider_manager", "Attempting provider switch", reason: reason, current: current_provider, **context)
|
|
75
|
+
|
|
72
76
|
# Get fallback chain for current provider
|
|
73
77
|
provider_fallback_chain = fallback_chain(current_provider)
|
|
74
78
|
|
|
@@ -79,7 +83,10 @@ module Aidp
|
|
|
79
83
|
success = set_current_provider(next_provider, reason, context)
|
|
80
84
|
if success
|
|
81
85
|
log_provider_switch(current_provider, next_provider, reason, context)
|
|
86
|
+
Aidp.logger.info("provider_manager", "Provider switched successfully", from: current_provider, to: next_provider, reason: reason)
|
|
82
87
|
return next_provider
|
|
88
|
+
else
|
|
89
|
+
Aidp.logger.warn("provider_manager", "Failed to switch to provider", provider: next_provider, reason: reason)
|
|
83
90
|
end
|
|
84
91
|
end
|
|
85
92
|
|
|
@@ -107,14 +114,21 @@ module Aidp
|
|
|
107
114
|
|
|
108
115
|
# No providers available
|
|
109
116
|
log_no_providers_available(reason, context)
|
|
117
|
+
Aidp.logger.error("provider_manager", "No providers available for fallback", reason: reason, **context)
|
|
110
118
|
nil
|
|
111
119
|
end
|
|
112
120
|
|
|
113
121
|
# Switch provider for specific error type
|
|
114
122
|
def switch_provider_for_error(error_type, error_details = {})
|
|
123
|
+
Aidp.logger.warn("provider_manager", "Error triggered provider switch", error_type: error_type, **error_details)
|
|
124
|
+
|
|
115
125
|
case error_type
|
|
116
126
|
when "rate_limit"
|
|
117
127
|
switch_provider("rate_limit", error_details)
|
|
128
|
+
when "resource_exhausted", "quota_exceeded"
|
|
129
|
+
# Treat capacity/resource exhaustion like rate limit for fallback purposes
|
|
130
|
+
Aidp.logger.warn("provider_manager", "Resource/quota exhaustion detected", classified_from: error_type)
|
|
131
|
+
switch_provider("rate_limit", error_details.merge(classified_from: error_type))
|
|
118
132
|
when "authentication"
|
|
119
133
|
switch_provider("authentication_error", error_details)
|
|
120
134
|
when "network"
|
|
@@ -269,9 +283,27 @@ module Aidp
|
|
|
269
283
|
|
|
270
284
|
# Set current provider with enhanced validation
|
|
271
285
|
def set_current_provider(provider_name, reason = "manual_switch", context = {})
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
286
|
+
# Use provider_config for ConfigManager, provider_configured? for legacy Configuration
|
|
287
|
+
config_exists = if @configuration.respond_to?(:provider_config)
|
|
288
|
+
@configuration.provider_config(provider_name)
|
|
289
|
+
else
|
|
290
|
+
@configuration.provider_configured?(provider_name)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
unless config_exists
|
|
294
|
+
Aidp.logger.warn("provider_manager", "Provider not configured", provider: provider_name)
|
|
295
|
+
return false
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
unless is_provider_healthy?(provider_name)
|
|
299
|
+
Aidp.logger.warn("provider_manager", "Provider not healthy", provider: provider_name)
|
|
300
|
+
return false
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
if is_provider_circuit_breaker_open?(provider_name)
|
|
304
|
+
Aidp.logger.warn("provider_manager", "Provider circuit breaker open", provider: provider_name)
|
|
305
|
+
return false
|
|
306
|
+
end
|
|
275
307
|
|
|
276
308
|
# Update provider health
|
|
277
309
|
update_provider_health(provider_name, "switched_to")
|
|
@@ -292,6 +324,7 @@ module Aidp
|
|
|
292
324
|
@current_model = default_model(provider_name)
|
|
293
325
|
|
|
294
326
|
@current_provider = provider_name
|
|
327
|
+
Aidp.logger.info("provider_manager", "Provider activated", provider: provider_name, reason: reason)
|
|
295
328
|
true
|
|
296
329
|
end
|
|
297
330
|
|
|
@@ -468,11 +501,24 @@ module Aidp
|
|
|
468
501
|
# Build default fallback chain
|
|
469
502
|
def build_default_fallback_chain(provider_name)
|
|
470
503
|
all_providers = configured_providers
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
504
|
+
|
|
505
|
+
# Harness-defined explicit ordering has priority
|
|
506
|
+
harness_fallbacks = if @configuration.respond_to?(:fallback_providers)
|
|
507
|
+
Array(@configuration.fallback_providers).map(&:to_s)
|
|
508
|
+
else
|
|
509
|
+
[]
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
# Construct ordered chain:
|
|
513
|
+
# 1. current provider first
|
|
514
|
+
# 2. harness fallback providers (excluding current and de-duplicated)
|
|
515
|
+
# 3. any remaining configured providers not already listed
|
|
516
|
+
ordered = [provider_name]
|
|
517
|
+
ordered += harness_fallbacks.reject { |p| p == provider_name || ordered.include?(p) }
|
|
518
|
+
ordered += all_providers.reject { |p| ordered.include?(p) }
|
|
519
|
+
|
|
520
|
+
@fallback_chains[provider_name] = ordered
|
|
521
|
+
ordered
|
|
476
522
|
end
|
|
477
523
|
|
|
478
524
|
# Find next healthy provider in fallback chain
|
|
@@ -1008,7 +1054,8 @@ module Aidp
|
|
|
1008
1054
|
require_relative "../providers/cursor"
|
|
1009
1055
|
installed = Aidp::Providers::Cursor.available?
|
|
1010
1056
|
end
|
|
1011
|
-
rescue LoadError
|
|
1057
|
+
rescue LoadError => e
|
|
1058
|
+
log_rescue(e, component: "provider_manager", action: "check_provider_availability", fallback: false, provider: provider_name)
|
|
1012
1059
|
installed = false
|
|
1013
1060
|
end
|
|
1014
1061
|
@unavailable_cache[provider_name] = installed
|
|
@@ -1048,7 +1095,8 @@ module Aidp
|
|
|
1048
1095
|
end
|
|
1049
1096
|
path = begin
|
|
1050
1097
|
Aidp::Util.which(binary)
|
|
1051
|
-
rescue
|
|
1098
|
+
rescue => e
|
|
1099
|
+
log_rescue(e, component: "provider_manager", action: "locate_binary", fallback: nil, binary: binary)
|
|
1052
1100
|
nil
|
|
1053
1101
|
end
|
|
1054
1102
|
unless path
|
|
@@ -1074,13 +1122,15 @@ module Aidp
|
|
|
1074
1122
|
# Timeout -> kill
|
|
1075
1123
|
begin
|
|
1076
1124
|
Process.kill("TERM", pid)
|
|
1077
|
-
rescue
|
|
1125
|
+
rescue => e
|
|
1126
|
+
log_rescue(e, component: "provider_manager", action: "kill_timeout_process_term", fallback: nil, binary: binary, pid: pid)
|
|
1078
1127
|
nil
|
|
1079
1128
|
end
|
|
1080
1129
|
sleep 0.1
|
|
1081
1130
|
begin
|
|
1082
1131
|
Process.kill("KILL", pid)
|
|
1083
|
-
rescue
|
|
1132
|
+
rescue => e
|
|
1133
|
+
log_rescue(e, component: "provider_manager", action: "kill_timeout_process_kill", fallback: nil, binary: binary, pid: pid)
|
|
1084
1134
|
nil
|
|
1085
1135
|
end
|
|
1086
1136
|
ok = false
|
|
@@ -1093,6 +1143,7 @@ module Aidp
|
|
|
1093
1143
|
ok = true
|
|
1094
1144
|
end
|
|
1095
1145
|
rescue => e
|
|
1146
|
+
log_rescue(e, component: "provider_manager", action: "verify_binary_health", fallback: "binary_error", binary: binary)
|
|
1096
1147
|
ok = false
|
|
1097
1148
|
reason = e.class.name.downcase.include?("enoent") ? "binary_missing" : "binary_error"
|
|
1098
1149
|
end
|
|
@@ -1340,6 +1391,7 @@ module Aidp
|
|
|
1340
1391
|
}
|
|
1341
1392
|
}
|
|
1342
1393
|
rescue => e
|
|
1394
|
+
log_rescue(e, component: "provider_manager", action: "execute_with_provider", fallback: "error_result", provider: provider_type, prompt_length: prompt.length)
|
|
1343
1395
|
# Return error result
|
|
1344
1396
|
{
|
|
1345
1397
|
status: "error",
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "securerandom"
|
|
4
4
|
require "yaml"
|
|
5
5
|
require "fileutils"
|
|
6
|
+
require_relative "../rescue_logging"
|
|
6
7
|
|
|
7
8
|
module Aidp
|
|
8
9
|
module Jobs
|
|
@@ -10,6 +11,7 @@ module Aidp
|
|
|
10
11
|
# Runs harness in daemon process and tracks job metadata
|
|
11
12
|
class BackgroundRunner
|
|
12
13
|
include Aidp::MessageDisplay
|
|
14
|
+
include Aidp::RescueLogging
|
|
13
15
|
|
|
14
16
|
attr_reader :project_dir, :jobs_dir
|
|
15
17
|
|
|
@@ -55,6 +57,7 @@ module Aidp
|
|
|
55
57
|
puts "[#{Time.now}] Job completed with status: #{result[:status]}"
|
|
56
58
|
mark_job_completed(job_id, result)
|
|
57
59
|
rescue => e
|
|
60
|
+
log_rescue(e, component: "background_runner", action: "execute_job", fallback: "mark_failed", job_id: job_id, mode: mode)
|
|
58
61
|
puts "[#{Time.now}] Job failed with error: #{e.message}"
|
|
59
62
|
puts e.backtrace.join("\n")
|
|
60
63
|
mark_job_failed(job_id, e)
|
|
@@ -136,11 +139,13 @@ module Aidp
|
|
|
136
139
|
|
|
137
140
|
mark_job_stopped(job_id)
|
|
138
141
|
{success: true, message: "Job stopped successfully"}
|
|
139
|
-
rescue Errno::ESRCH
|
|
142
|
+
rescue Errno::ESRCH => e
|
|
143
|
+
log_rescue(e, component: "background_runner", action: "stop_job", fallback: "mark_stopped", job_id: job_id, pid: pid, level: :info)
|
|
140
144
|
# Process already dead
|
|
141
145
|
mark_job_stopped(job_id)
|
|
142
146
|
{success: true, message: "Job was already stopped"}
|
|
143
147
|
rescue => e
|
|
148
|
+
log_rescue(e, component: "background_runner", action: "stop_job", fallback: "error_result", job_id: job_id, pid: pid)
|
|
144
149
|
{success: false, message: "Failed to stop job: #{e.message}"}
|
|
145
150
|
end
|
|
146
151
|
end
|
|
@@ -244,7 +249,8 @@ module Aidp
|
|
|
244
249
|
|
|
245
250
|
Process.kill(0, pid)
|
|
246
251
|
true
|
|
247
|
-
rescue Errno::ESRCH, Errno::EPERM
|
|
252
|
+
rescue Errno::ESRCH, Errno::EPERM => e
|
|
253
|
+
log_rescue(e, component: "background_runner", action: "check_process_running", fallback: false, pid: pid, level: :debug)
|
|
248
254
|
false
|
|
249
255
|
end
|
|
250
256
|
|
|
@@ -252,7 +258,8 @@ module Aidp
|
|
|
252
258
|
# Try to load checkpoint from project directory
|
|
253
259
|
checkpoint = Aidp::Execute::Checkpoint.new(@project_dir)
|
|
254
260
|
checkpoint.latest_checkpoint
|
|
255
|
-
rescue
|
|
261
|
+
rescue => e
|
|
262
|
+
log_rescue(e, component: "background_runner", action: "get_job_checkpoint", fallback: nil, job_id: job_id)
|
|
256
263
|
nil
|
|
257
264
|
end
|
|
258
265
|
|