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
|
@@ -7,6 +7,8 @@ module Aidp
|
|
|
7
7
|
module Harness
|
|
8
8
|
# Handles loading and validation of harness configuration from aidp.yml
|
|
9
9
|
class Configuration
|
|
10
|
+
attr_reader :project_dir
|
|
11
|
+
|
|
10
12
|
def initialize(project_dir)
|
|
11
13
|
@project_dir = project_dir
|
|
12
14
|
@config = Aidp::Config.load_harness_config(project_dir)
|
|
@@ -600,7 +602,16 @@ module Aidp
|
|
|
600
602
|
|
|
601
603
|
# Get devcontainer configuration
|
|
602
604
|
def devcontainer_config
|
|
603
|
-
@
|
|
605
|
+
return @devcontainer_config if defined?(@devcontainer_config)
|
|
606
|
+
|
|
607
|
+
raw_config = @config[:devcontainer] || @config["devcontainer"]
|
|
608
|
+
base = deep_dup(default_devcontainer_config)
|
|
609
|
+
|
|
610
|
+
@devcontainer_config = if raw_config.is_a?(Hash)
|
|
611
|
+
deep_merge_hashes(base, deep_symbolize_keys(raw_config))
|
|
612
|
+
else
|
|
613
|
+
base
|
|
614
|
+
end
|
|
604
615
|
end
|
|
605
616
|
|
|
606
617
|
# Check if devcontainer features are enabled
|
|
@@ -629,7 +640,10 @@ module Aidp
|
|
|
629
640
|
|
|
630
641
|
# Get devcontainer permissions config
|
|
631
642
|
def devcontainer_permissions
|
|
632
|
-
devcontainer_config[:permissions]
|
|
643
|
+
permissions = devcontainer_config[:permissions]
|
|
644
|
+
return {} unless permissions.is_a?(Hash)
|
|
645
|
+
|
|
646
|
+
permissions.transform_keys { |key| key.to_sym }
|
|
633
647
|
end
|
|
634
648
|
|
|
635
649
|
# Check if dangerous filesystem operations are allowed in devcontainer
|
|
@@ -639,7 +653,15 @@ module Aidp
|
|
|
639
653
|
|
|
640
654
|
# Get list of providers that should skip permission checks in devcontainer
|
|
641
655
|
def devcontainer_skip_permission_checks
|
|
642
|
-
|
|
656
|
+
permissions = devcontainer_config[:permissions]
|
|
657
|
+
list = nil
|
|
658
|
+
|
|
659
|
+
if permissions.is_a?(Hash)
|
|
660
|
+
list = permissions[:skip_permission_checks] || permissions["skip_permission_checks"]
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
list = default_skip_permission_checks if list.nil?
|
|
664
|
+
Array(list).map(&:to_s)
|
|
643
665
|
end
|
|
644
666
|
|
|
645
667
|
# Check if a specific provider should skip permission checks in devcontainer
|
|
@@ -843,7 +865,7 @@ module Aidp
|
|
|
843
865
|
max_backoff_seconds: 1800,
|
|
844
866
|
next: {
|
|
845
867
|
success: :agentic,
|
|
846
|
-
failure: :
|
|
868
|
+
failure: :diagnose_failures,
|
|
847
869
|
else: :decide_whats_next
|
|
848
870
|
}
|
|
849
871
|
},
|
|
@@ -856,7 +878,7 @@ module Aidp
|
|
|
856
878
|
max_backoff_seconds: 1800,
|
|
857
879
|
next: {
|
|
858
880
|
success: :agentic,
|
|
859
|
-
failure: :
|
|
881
|
+
failure: :diagnose_failures,
|
|
860
882
|
else: :decide_whats_next
|
|
861
883
|
}
|
|
862
884
|
},
|
|
@@ -1055,7 +1077,7 @@ module Aidp
|
|
|
1055
1077
|
force_detection: nil,
|
|
1056
1078
|
permissions: {
|
|
1057
1079
|
dangerous_filesystem_ops: false,
|
|
1058
|
-
skip_permission_checks: []
|
|
1080
|
+
skip_permission_checks: ["claude"]
|
|
1059
1081
|
},
|
|
1060
1082
|
settings: {
|
|
1061
1083
|
timeout_multiplier: 1.0,
|
|
@@ -1065,6 +1087,47 @@ module Aidp
|
|
|
1065
1087
|
}
|
|
1066
1088
|
end
|
|
1067
1089
|
|
|
1090
|
+
def default_skip_permission_checks
|
|
1091
|
+
Array(default_devcontainer_config.dig(:permissions, :skip_permission_checks)).map(&:to_s)
|
|
1092
|
+
end
|
|
1093
|
+
|
|
1094
|
+
def deep_symbolize_keys(value)
|
|
1095
|
+
case value
|
|
1096
|
+
when Hash
|
|
1097
|
+
value.each_with_object({}) do |(key, val), memo|
|
|
1098
|
+
memo[key.to_sym] = deep_symbolize_keys(val)
|
|
1099
|
+
end
|
|
1100
|
+
when Array
|
|
1101
|
+
value.map { |item| deep_symbolize_keys(item) }
|
|
1102
|
+
else
|
|
1103
|
+
value
|
|
1104
|
+
end
|
|
1105
|
+
end
|
|
1106
|
+
|
|
1107
|
+
def deep_merge_hashes(base, overrides)
|
|
1108
|
+
overrides.each do |key, value|
|
|
1109
|
+
base[key] = if base[key].is_a?(Hash) && value.is_a?(Hash)
|
|
1110
|
+
deep_merge_hashes(base[key], value)
|
|
1111
|
+
else
|
|
1112
|
+
value
|
|
1113
|
+
end
|
|
1114
|
+
end
|
|
1115
|
+
base
|
|
1116
|
+
end
|
|
1117
|
+
|
|
1118
|
+
def deep_dup(value)
|
|
1119
|
+
case value
|
|
1120
|
+
when Hash
|
|
1121
|
+
value.each_with_object({}) do |(key, val), memo|
|
|
1122
|
+
memo[key] = deep_dup(val)
|
|
1123
|
+
end
|
|
1124
|
+
when Array
|
|
1125
|
+
value.map { |item| deep_dup(item) }
|
|
1126
|
+
else
|
|
1127
|
+
value
|
|
1128
|
+
end
|
|
1129
|
+
end
|
|
1130
|
+
|
|
1068
1131
|
# Custom error class for configuration issues
|
|
1069
1132
|
class ConfigurationError < StandardError; end
|
|
1070
1133
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "net/http"
|
|
4
4
|
require_relative "../debug_mixin"
|
|
5
5
|
require_relative "../concurrency"
|
|
6
|
+
require_relative "../providers/error_taxonomy"
|
|
6
7
|
|
|
7
8
|
module Aidp
|
|
8
9
|
module Harness
|
|
@@ -252,9 +253,10 @@ module Aidp
|
|
|
252
253
|
# Check if we should retry based on error type and strategy
|
|
253
254
|
def should_retry?(error_info, strategy)
|
|
254
255
|
return false unless strategy[:enabled]
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
256
|
+
|
|
257
|
+
# Use ErrorTaxonomy to determine if error is retryable
|
|
258
|
+
error_type = error_info[:error_type]
|
|
259
|
+
return false unless Aidp::Providers::ErrorTaxonomy.retryable?(error_type)
|
|
258
260
|
|
|
259
261
|
# Check circuit breaker
|
|
260
262
|
circuit_breaker_key = "#{error_info[:provider]}:#{error_info[:model]}"
|
|
@@ -337,7 +339,62 @@ module Aidp
|
|
|
337
339
|
|
|
338
340
|
def initialize_retry_strategies
|
|
339
341
|
@retry_strategies = {
|
|
340
|
-
#
|
|
342
|
+
# Transient errors - retry with exponential backoff
|
|
343
|
+
transient: {
|
|
344
|
+
name: "transient",
|
|
345
|
+
enabled: true,
|
|
346
|
+
max_retries: 3,
|
|
347
|
+
backoff_strategy: :exponential,
|
|
348
|
+
base_delay: 1.0,
|
|
349
|
+
max_delay: 30.0,
|
|
350
|
+
jitter: true
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
# Rate limited errors - no retry, immediate switch
|
|
354
|
+
rate_limited: {
|
|
355
|
+
name: "rate_limited",
|
|
356
|
+
enabled: false,
|
|
357
|
+
max_retries: 0,
|
|
358
|
+
backoff_strategy: :none,
|
|
359
|
+
base_delay: 0.0,
|
|
360
|
+
max_delay: 0.0,
|
|
361
|
+
jitter: false
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
# Authentication expired - no retry, switch provider
|
|
365
|
+
auth_expired: {
|
|
366
|
+
name: "auth_expired",
|
|
367
|
+
enabled: false,
|
|
368
|
+
max_retries: 0,
|
|
369
|
+
backoff_strategy: :none,
|
|
370
|
+
base_delay: 0.0,
|
|
371
|
+
max_delay: 0.0,
|
|
372
|
+
jitter: false
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
# Quota exceeded - no retry, switch provider
|
|
376
|
+
quota_exceeded: {
|
|
377
|
+
name: "quota_exceeded",
|
|
378
|
+
enabled: false,
|
|
379
|
+
max_retries: 0,
|
|
380
|
+
backoff_strategy: :none,
|
|
381
|
+
base_delay: 0.0,
|
|
382
|
+
max_delay: 0.0,
|
|
383
|
+
jitter: false
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
# Permanent errors - no retry, escalate
|
|
387
|
+
permanent: {
|
|
388
|
+
name: "permanent",
|
|
389
|
+
enabled: false,
|
|
390
|
+
max_retries: 0,
|
|
391
|
+
backoff_strategy: :none,
|
|
392
|
+
base_delay: 0.0,
|
|
393
|
+
max_delay: 0.0,
|
|
394
|
+
jitter: false
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
# Legacy aliases for backward compatibility
|
|
341
398
|
network_error: {
|
|
342
399
|
name: "network_error",
|
|
343
400
|
enabled: true,
|
|
@@ -347,8 +404,6 @@ module Aidp
|
|
|
347
404
|
max_delay: 30.0,
|
|
348
405
|
jitter: true
|
|
349
406
|
},
|
|
350
|
-
|
|
351
|
-
# Server errors - retry with linear backoff
|
|
352
407
|
server_error: {
|
|
353
408
|
name: "server_error",
|
|
354
409
|
enabled: true,
|
|
@@ -358,8 +413,6 @@ module Aidp
|
|
|
358
413
|
max_delay: 10.0,
|
|
359
414
|
jitter: true
|
|
360
415
|
},
|
|
361
|
-
|
|
362
|
-
# Timeout errors - retry with exponential backoff
|
|
363
416
|
timeout: {
|
|
364
417
|
name: "timeout",
|
|
365
418
|
enabled: true,
|
|
@@ -369,8 +422,6 @@ module Aidp
|
|
|
369
422
|
max_delay: 15.0,
|
|
370
423
|
jitter: true
|
|
371
424
|
},
|
|
372
|
-
|
|
373
|
-
# Rate limit errors - no retry, immediate switch
|
|
374
425
|
rate_limit: {
|
|
375
426
|
name: "rate_limit",
|
|
376
427
|
enabled: false,
|
|
@@ -380,8 +431,6 @@ module Aidp
|
|
|
380
431
|
max_delay: 0.0,
|
|
381
432
|
jitter: false
|
|
382
433
|
},
|
|
383
|
-
|
|
384
|
-
# Authentication errors - no retry, escalate
|
|
385
434
|
authentication: {
|
|
386
435
|
name: "authentication",
|
|
387
436
|
enabled: false,
|
|
@@ -391,8 +440,6 @@ module Aidp
|
|
|
391
440
|
max_delay: 0.0,
|
|
392
441
|
jitter: false
|
|
393
442
|
},
|
|
394
|
-
|
|
395
|
-
# Permission denied - no retry, escalate
|
|
396
443
|
permission_denied: {
|
|
397
444
|
name: "permission_denied",
|
|
398
445
|
enabled: false,
|
|
@@ -586,41 +633,36 @@ module Aidp
|
|
|
586
633
|
private
|
|
587
634
|
|
|
588
635
|
def classify_error_type(error)
|
|
589
|
-
return :
|
|
636
|
+
return :transient if error.nil?
|
|
637
|
+
|
|
638
|
+
# Use standardized error taxonomy for classification
|
|
639
|
+
message = error.message.to_s
|
|
590
640
|
|
|
641
|
+
# First, use ErrorTaxonomy to classify by message
|
|
642
|
+
category = Aidp::Providers::ErrorTaxonomy.classify_message(message)
|
|
643
|
+
|
|
644
|
+
# Override with more specific classification based on error type
|
|
591
645
|
case error
|
|
592
646
|
when Timeout::Error
|
|
593
|
-
:
|
|
647
|
+
:transient
|
|
594
648
|
when Net::HTTPError
|
|
595
649
|
case error.response.code.to_i
|
|
596
650
|
when 429
|
|
597
|
-
:
|
|
651
|
+
:rate_limited
|
|
598
652
|
when 401, 403
|
|
599
|
-
:
|
|
653
|
+
:auth_expired
|
|
600
654
|
when 500..599
|
|
601
|
-
:
|
|
655
|
+
:transient
|
|
656
|
+
when 400
|
|
657
|
+
:permanent
|
|
602
658
|
else
|
|
603
|
-
:
|
|
659
|
+
:transient
|
|
604
660
|
end
|
|
605
661
|
when SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH
|
|
606
|
-
:
|
|
607
|
-
when StandardError
|
|
608
|
-
# Check error message for common patterns
|
|
609
|
-
message = error.message.downcase
|
|
610
|
-
|
|
611
|
-
if message.include?("rate limit") || message.include?("quota")
|
|
612
|
-
:rate_limit
|
|
613
|
-
elsif message.include?("timeout")
|
|
614
|
-
:timeout
|
|
615
|
-
elsif message.include?("auth") || message.include?("permission")
|
|
616
|
-
:authentication
|
|
617
|
-
elsif message.include?("server") || message.include?("internal")
|
|
618
|
-
:server_error
|
|
619
|
-
else
|
|
620
|
-
:default
|
|
621
|
-
end
|
|
662
|
+
:transient
|
|
622
663
|
else
|
|
623
|
-
|
|
664
|
+
# Use message-based classification from ErrorTaxonomy
|
|
665
|
+
category
|
|
624
666
|
end
|
|
625
667
|
end
|
|
626
668
|
end
|
|
@@ -629,22 +671,41 @@ module Aidp
|
|
|
629
671
|
def create_recovery_plan(error_info, _context = {})
|
|
630
672
|
error_type = error_info[:error_type]
|
|
631
673
|
|
|
674
|
+
# Use ErrorTaxonomy to determine recovery strategy
|
|
632
675
|
case error_type
|
|
633
|
-
when :
|
|
676
|
+
when :rate_limited
|
|
634
677
|
{
|
|
635
678
|
action: :switch_provider,
|
|
636
679
|
reason: "Rate limit reached, switching provider",
|
|
637
680
|
priority: :high
|
|
638
681
|
}
|
|
639
|
-
when :
|
|
640
|
-
#
|
|
641
|
-
#
|
|
642
|
-
# while the user resolves credentials for the failing provider.
|
|
682
|
+
when :auth_expired
|
|
683
|
+
# Attempt a provider switch so workflows can continue with alternate providers
|
|
684
|
+
# while the user resolves credentials for the failing provider
|
|
643
685
|
{
|
|
644
686
|
action: :switch_provider,
|
|
645
|
-
reason: "Authentication
|
|
687
|
+
reason: "Authentication expired – switching provider to continue",
|
|
646
688
|
priority: :critical
|
|
647
689
|
}
|
|
690
|
+
when :quota_exceeded
|
|
691
|
+
{
|
|
692
|
+
action: :switch_provider,
|
|
693
|
+
reason: "Quota exceeded, switching provider",
|
|
694
|
+
priority: :high
|
|
695
|
+
}
|
|
696
|
+
when :transient
|
|
697
|
+
{
|
|
698
|
+
action: :switch_model,
|
|
699
|
+
reason: "Transient error, trying alternate model",
|
|
700
|
+
priority: :medium
|
|
701
|
+
}
|
|
702
|
+
when :permanent
|
|
703
|
+
{
|
|
704
|
+
action: :escalate,
|
|
705
|
+
reason: "Permanent error, requires manual intervention",
|
|
706
|
+
priority: :critical
|
|
707
|
+
}
|
|
708
|
+
# Legacy error type mappings for backward compatibility
|
|
648
709
|
when :timeout
|
|
649
710
|
{
|
|
650
711
|
action: :switch_model,
|
|
@@ -655,7 +716,7 @@ module Aidp
|
|
|
655
716
|
{
|
|
656
717
|
action: :switch_provider,
|
|
657
718
|
reason: "Network error, switching provider",
|
|
658
|
-
priority: :
|
|
719
|
+
priority: :medium
|
|
659
720
|
}
|
|
660
721
|
when :server_error
|
|
661
722
|
{
|
|
@@ -663,6 +724,18 @@ module Aidp
|
|
|
663
724
|
reason: "Server error, switching provider",
|
|
664
725
|
priority: :medium
|
|
665
726
|
}
|
|
727
|
+
when :authentication, :permission_denied
|
|
728
|
+
{
|
|
729
|
+
action: :switch_provider,
|
|
730
|
+
reason: "Authentication/permission issue – switching provider to continue",
|
|
731
|
+
priority: :critical
|
|
732
|
+
}
|
|
733
|
+
when :rate_limit
|
|
734
|
+
{
|
|
735
|
+
action: :switch_provider,
|
|
736
|
+
reason: "Rate limit reached, switching provider",
|
|
737
|
+
priority: :high
|
|
738
|
+
}
|
|
666
739
|
else
|
|
667
740
|
{
|
|
668
741
|
action: :switch_provider,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "tty-prompt"
|
|
4
4
|
require_relative "provider_factory"
|
|
5
|
+
require_relative "provider_metrics"
|
|
5
6
|
require_relative "../rescue_logging"
|
|
6
7
|
require_relative "../concurrency"
|
|
7
8
|
|
|
@@ -40,6 +41,20 @@ module Aidp
|
|
|
40
41
|
@unavailable_cache = {}
|
|
41
42
|
@binary_check_cache = {}
|
|
42
43
|
@binary_check_ttl = 300 # seconds
|
|
44
|
+
|
|
45
|
+
# Initialize persistence
|
|
46
|
+
project_dir = if configuration.respond_to?(:project_dir)
|
|
47
|
+
configuration.project_dir
|
|
48
|
+
elsif configuration.respond_to?(:root_dir)
|
|
49
|
+
configuration.root_dir
|
|
50
|
+
else
|
|
51
|
+
Dir.pwd
|
|
52
|
+
end
|
|
53
|
+
@metrics_persistence = ProviderMetrics.new(project_dir)
|
|
54
|
+
|
|
55
|
+
# Load persisted metrics
|
|
56
|
+
load_persisted_metrics
|
|
57
|
+
|
|
43
58
|
initialize_fallback_chains
|
|
44
59
|
initialize_provider_health
|
|
45
60
|
initialize_model_configs
|
|
@@ -932,6 +947,9 @@ module Aidp
|
|
|
932
947
|
# Update provider health
|
|
933
948
|
update_provider_health(provider_name, "rate_limited")
|
|
934
949
|
|
|
950
|
+
# Persist rate limit info to disk
|
|
951
|
+
save_persisted_rate_limits
|
|
952
|
+
|
|
935
953
|
# Switch to next provider if current one is rate limited
|
|
936
954
|
if provider_name == current_provider
|
|
937
955
|
switch_provider("rate_limit", {provider: provider_name})
|
|
@@ -996,6 +1014,9 @@ module Aidp
|
|
|
996
1014
|
metrics[:last_error_time] = Time.now
|
|
997
1015
|
update_provider_health(provider_name, "error", {error: error})
|
|
998
1016
|
end
|
|
1017
|
+
|
|
1018
|
+
# Persist metrics to disk
|
|
1019
|
+
save_persisted_metrics
|
|
999
1020
|
end
|
|
1000
1021
|
|
|
1001
1022
|
# Record model metrics
|
|
@@ -1611,6 +1632,49 @@ module Aidp
|
|
|
1611
1632
|
# Most models reset rate limits every hour
|
|
1612
1633
|
Time.now + (60 * 60)
|
|
1613
1634
|
end
|
|
1635
|
+
|
|
1636
|
+
# Load persisted metrics from disk
|
|
1637
|
+
def load_persisted_metrics
|
|
1638
|
+
return unless @metrics_persistence
|
|
1639
|
+
|
|
1640
|
+
# Load provider metrics
|
|
1641
|
+
persisted_metrics = @metrics_persistence.load_metrics
|
|
1642
|
+
@provider_metrics.merge!(persisted_metrics) if persisted_metrics.is_a?(Hash)
|
|
1643
|
+
|
|
1644
|
+
# Load rate limit info
|
|
1645
|
+
persisted_rate_limits = @metrics_persistence.load_rate_limits
|
|
1646
|
+
@rate_limit_info.merge!(persisted_rate_limits) if persisted_rate_limits.is_a?(Hash)
|
|
1647
|
+
|
|
1648
|
+
# Clean up expired rate limits
|
|
1649
|
+
cleanup_expired_rate_limits
|
|
1650
|
+
rescue => e
|
|
1651
|
+
log_rescue(e, component: "provider_manager", action: "load_persisted_metrics", fallback: nil)
|
|
1652
|
+
end
|
|
1653
|
+
|
|
1654
|
+
# Save persisted metrics to disk
|
|
1655
|
+
def save_persisted_metrics
|
|
1656
|
+
return unless @metrics_persistence
|
|
1657
|
+
@metrics_persistence.save_metrics(@provider_metrics)
|
|
1658
|
+
rescue => e
|
|
1659
|
+
log_rescue(e, component: "provider_manager", action: "save_persisted_metrics", fallback: nil)
|
|
1660
|
+
end
|
|
1661
|
+
|
|
1662
|
+
# Save persisted rate limits to disk
|
|
1663
|
+
def save_persisted_rate_limits
|
|
1664
|
+
return unless @metrics_persistence
|
|
1665
|
+
@metrics_persistence.save_rate_limits(@rate_limit_info)
|
|
1666
|
+
rescue => e
|
|
1667
|
+
log_rescue(e, component: "provider_manager", action: "save_persisted_rate_limits", fallback: nil)
|
|
1668
|
+
end
|
|
1669
|
+
|
|
1670
|
+
# Clean up expired rate limits from memory
|
|
1671
|
+
def cleanup_expired_rate_limits
|
|
1672
|
+
now = Time.now
|
|
1673
|
+
@rate_limit_info.delete_if do |_provider, info|
|
|
1674
|
+
reset_time = info[:reset_time]
|
|
1675
|
+
reset_time && now >= reset_time
|
|
1676
|
+
end
|
|
1677
|
+
end
|
|
1614
1678
|
end
|
|
1615
1679
|
end
|
|
1616
1680
|
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require_relative "../rescue_logging"
|
|
6
|
+
|
|
7
|
+
module Aidp
|
|
8
|
+
module Harness
|
|
9
|
+
# Persists provider metrics and rate limit information to disk
|
|
10
|
+
# Enables the provider dashboard to display real-time state
|
|
11
|
+
class ProviderMetrics
|
|
12
|
+
include Aidp::RescueLogging
|
|
13
|
+
|
|
14
|
+
attr_reader :project_dir, :metrics_file, :rate_limit_file
|
|
15
|
+
|
|
16
|
+
def initialize(project_dir)
|
|
17
|
+
@project_dir = project_dir
|
|
18
|
+
@metrics_file = File.join(project_dir, ".aidp", "provider_metrics.yml")
|
|
19
|
+
@rate_limit_file = File.join(project_dir, ".aidp", "provider_rate_limits.yml")
|
|
20
|
+
ensure_directory
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Save provider metrics to disk
|
|
24
|
+
def save_metrics(metrics_hash)
|
|
25
|
+
return if metrics_hash.nil? || metrics_hash.empty?
|
|
26
|
+
|
|
27
|
+
# Convert Time objects to ISO8601 strings for YAML serialization
|
|
28
|
+
serializable_metrics = serialize_metrics(metrics_hash)
|
|
29
|
+
|
|
30
|
+
File.write(@metrics_file, YAML.dump(serializable_metrics))
|
|
31
|
+
rescue => e
|
|
32
|
+
log_rescue(e, component: "provider_metrics", action: "save_metrics", fallback: nil)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Load provider metrics from disk
|
|
36
|
+
def load_metrics
|
|
37
|
+
return {} unless File.exist?(@metrics_file)
|
|
38
|
+
|
|
39
|
+
data = YAML.safe_load_file(@metrics_file, permitted_classes: [Time, Date, Symbol], aliases: true)
|
|
40
|
+
return {} unless data.is_a?(Hash)
|
|
41
|
+
|
|
42
|
+
# Convert ISO8601 strings back to Time objects
|
|
43
|
+
deserialize_metrics(data)
|
|
44
|
+
rescue => e
|
|
45
|
+
log_rescue(e, component: "provider_metrics", action: "load_metrics", fallback: {})
|
|
46
|
+
{}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Save rate limit information to disk
|
|
50
|
+
def save_rate_limits(rate_limit_hash)
|
|
51
|
+
return if rate_limit_hash.nil? || rate_limit_hash.empty?
|
|
52
|
+
|
|
53
|
+
# Convert Time objects to ISO8601 strings for YAML serialization
|
|
54
|
+
serializable_rate_limits = serialize_rate_limits(rate_limit_hash)
|
|
55
|
+
|
|
56
|
+
File.write(@rate_limit_file, YAML.dump(serializable_rate_limits))
|
|
57
|
+
rescue => e
|
|
58
|
+
log_rescue(e, component: "provider_metrics", action: "save_rate_limits", fallback: nil)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Load rate limit information from disk
|
|
62
|
+
def load_rate_limits
|
|
63
|
+
return {} unless File.exist?(@rate_limit_file)
|
|
64
|
+
|
|
65
|
+
data = YAML.safe_load_file(@rate_limit_file, permitted_classes: [Time, Date, Symbol], aliases: true)
|
|
66
|
+
return {} unless data.is_a?(Hash)
|
|
67
|
+
|
|
68
|
+
# Convert ISO8601 strings back to Time objects
|
|
69
|
+
deserialize_rate_limits(data)
|
|
70
|
+
rescue => e
|
|
71
|
+
log_rescue(e, component: "provider_metrics", action: "load_rate_limits", fallback: {})
|
|
72
|
+
{}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Clear all persisted metrics
|
|
76
|
+
def clear
|
|
77
|
+
File.delete(@metrics_file) if File.exist?(@metrics_file)
|
|
78
|
+
File.delete(@rate_limit_file) if File.exist?(@rate_limit_file)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def ensure_directory
|
|
84
|
+
aidp_dir = File.join(@project_dir, ".aidp")
|
|
85
|
+
FileUtils.mkdir_p(aidp_dir) unless File.directory?(aidp_dir)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def serialize_metrics(metrics_hash)
|
|
89
|
+
metrics_hash.transform_values do |provider_metrics|
|
|
90
|
+
next provider_metrics unless provider_metrics.is_a?(Hash)
|
|
91
|
+
|
|
92
|
+
provider_metrics.transform_values do |value|
|
|
93
|
+
value.is_a?(Time) ? value.iso8601 : value
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def deserialize_metrics(metrics_hash)
|
|
99
|
+
metrics_hash.transform_values do |provider_metrics|
|
|
100
|
+
next provider_metrics unless provider_metrics.is_a?(Hash)
|
|
101
|
+
|
|
102
|
+
provider_metrics.transform_keys(&:to_sym).transform_values do |value|
|
|
103
|
+
parse_time_if_string(value)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def serialize_rate_limits(rate_limit_hash)
|
|
109
|
+
rate_limit_hash.transform_values do |limit_info|
|
|
110
|
+
next limit_info unless limit_info.is_a?(Hash)
|
|
111
|
+
|
|
112
|
+
limit_info.transform_values do |value|
|
|
113
|
+
value.is_a?(Time) ? value.iso8601 : value
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def deserialize_rate_limits(rate_limit_hash)
|
|
119
|
+
rate_limit_hash.transform_values do |limit_info|
|
|
120
|
+
next limit_info unless limit_info.is_a?(Hash)
|
|
121
|
+
|
|
122
|
+
limit_info.transform_keys(&:to_sym).transform_values do |value|
|
|
123
|
+
parse_time_if_string(value)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def parse_time_if_string(value)
|
|
129
|
+
return value unless value.is_a?(String)
|
|
130
|
+
|
|
131
|
+
# Try to parse ISO8601 timestamp
|
|
132
|
+
Time.parse(value)
|
|
133
|
+
rescue ArgumentError
|
|
134
|
+
value
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|