lex-resilience 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c568cb863f7bce59fa4b9671135e36e5ac16142915c271f217e97f926810257c
4
+ data.tar.gz: b727773a253036e8ff144cade567ae6ade65933f760e0febf188a53242e8d0e1
5
+ SHA512:
6
+ metadata.gz: e9ec2c68dfa1d5fd33a7f1160b2650c5d0d89e27354b8862e5cda540c94bd71a0f18682870583260f322c6c3c075311c856bc0902fece25f620d012046f17660
7
+ data.tar.gz: 3fca179ba62761d865ef34aa12473585990bb6a7456929eb3961c2beec2dd5ae6d4ff06d976b476e475d4804c91c51fec341881a0b4dcff9b5e55b113ac58947
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/resilience/helpers/constants'
4
+ require 'legion/extensions/resilience/helpers/adversity_tracker'
5
+ require 'legion/extensions/resilience/helpers/resilience_model'
6
+ require 'legion/extensions/resilience/runners/resilience'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module Resilience
11
+ class Client
12
+ include Runners::Resilience
13
+
14
+ attr_reader :adversity_tracker, :resilience_model
15
+
16
+ def initialize(adversity_tracker: nil, resilience_model: nil, **)
17
+ @adversity_tracker = adversity_tracker || Helpers::AdversityTracker.new
18
+ @resilience_model = resilience_model || Helpers::ResilienceModel.new
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Resilience
6
+ module Helpers
7
+ class AdversityTracker
8
+ attr_reader :active_adversities, :resolved_adversities, :consecutive_recoveries
9
+
10
+ def initialize
11
+ @active_adversities = []
12
+ @resolved_adversities = []
13
+ @consecutive_recoveries = 0
14
+ @adversity_counter = 0
15
+ end
16
+
17
+ def register(type:, severity:, context: {})
18
+ return nil unless Constants::ADVERSITY_TYPES.include?(type)
19
+ return nil unless Constants::SEVERITY_LEVELS.key?(severity)
20
+
21
+ @adversity_counter += 1
22
+ severity_config = Constants::SEVERITY_LEVELS[severity]
23
+
24
+ adversity = {
25
+ id: @adversity_counter,
26
+ type: type,
27
+ severity: severity,
28
+ impact: severity_config[:impact],
29
+ expected_ticks: severity_config[:recovery_ticks],
30
+ phase: :absorbing,
31
+ health_at_onset: 1.0,
32
+ current_health: 1.0 - severity_config[:impact],
33
+ ticks_elapsed: 0,
34
+ context: context,
35
+ registered_at: Time.now.utc
36
+ }
37
+
38
+ @active_adversities << adversity
39
+ trim_active
40
+ adversity
41
+ end
42
+
43
+ def tick_recovery
44
+ @active_adversities.each do |adv|
45
+ adv[:ticks_elapsed] += 1
46
+ advance_phase(adv)
47
+ recover_health(adv)
48
+ end
49
+
50
+ newly_resolved = @active_adversities.select { |a| a[:current_health] >= Constants::RECOVERY_THRESHOLD }
51
+ newly_resolved.each { |a| resolve(a) }
52
+
53
+ {
54
+ active_count: @active_adversities.size,
55
+ resolved_count: newly_resolved.size,
56
+ worst_health: worst_health
57
+ }
58
+ end
59
+
60
+ def worst_health
61
+ return 1.0 if @active_adversities.empty?
62
+
63
+ @active_adversities.map { |a| a[:current_health] }.min
64
+ end
65
+
66
+ def active_by_type
67
+ @active_adversities.group_by { |a| a[:type] }.transform_values(&:size)
68
+ end
69
+
70
+ def recovery_rate
71
+ total = @resolved_adversities.size
72
+ return 0.0 if total.zero?
73
+
74
+ on_time = @resolved_adversities.count { |a| a[:ticks_elapsed] <= a[:expected_ticks] }
75
+ on_time.to_f / total
76
+ end
77
+
78
+ def average_recovery_speed
79
+ return 0.0 if @resolved_adversities.empty?
80
+
81
+ ratios = @resolved_adversities.map { |a| a[:ticks_elapsed].to_f / [a[:expected_ticks], 1].max }
82
+ ratios.sum / ratios.size.to_f
83
+ end
84
+
85
+ def total_adversities
86
+ @active_adversities.size + @resolved_adversities.size
87
+ end
88
+
89
+ private
90
+
91
+ def advance_phase(adv)
92
+ progress = adv[:current_health]
93
+ adv[:phase] = if progress < 0.3
94
+ :absorbing
95
+ elsif progress < 0.6
96
+ :adapting
97
+ elsif progress < Constants::RECOVERY_THRESHOLD
98
+ :recovering
99
+ else
100
+ :thriving
101
+ end
102
+ end
103
+
104
+ def recover_health(adv)
105
+ recovery_rate = 1.0 / [adv[:expected_ticks], 1].max
106
+ adv[:current_health] = [adv[:current_health] + recovery_rate, 1.0].min
107
+ end
108
+
109
+ def resolve(adv)
110
+ adv[:phase] = :thriving
111
+ adv[:resolved_at] = Time.now.utc
112
+ @active_adversities.delete(adv)
113
+ @resolved_adversities << adv
114
+ @resolved_adversities.shift while @resolved_adversities.size > Constants::MAX_RESILIENCE_HISTORY
115
+
116
+ @consecutive_recoveries += 1
117
+ end
118
+
119
+ def trim_active
120
+ @active_adversities.shift while @active_adversities.size > Constants::MAX_ACTIVE_ADVERSITIES
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Resilience
6
+ module Helpers
7
+ module Constants
8
+ # Adversity types the resilience system tracks
9
+ ADVERSITY_TYPES = %i[
10
+ prediction_failure
11
+ trust_violation
12
+ conflict_escalation
13
+ resource_depletion
14
+ communication_failure
15
+ goal_failure
16
+ emotional_shock
17
+ system_error
18
+ ].freeze
19
+
20
+ # Recovery phases (Masten's resilience model)
21
+ RECOVERY_PHASES = %i[
22
+ absorbing
23
+ adapting
24
+ recovering
25
+ thriving
26
+ ].freeze
27
+
28
+ # Resilience dimensions
29
+ DIMENSIONS = {
30
+ elasticity: { description: 'Speed of recovery to baseline', weight: 0.30 },
31
+ robustness: { description: 'Resistance to initial disruption', weight: 0.25 },
32
+ adaptability: { description: 'Capacity to adjust strategy', weight: 0.25 },
33
+ growth: { description: 'Ability to improve from adversity', weight: 0.20 }
34
+ }.freeze
35
+
36
+ # EMA alpha for resilience dimension tracking
37
+ RESILIENCE_ALPHA = 0.08
38
+
39
+ # Growth bonus per successful recovery
40
+ GROWTH_INCREMENT = 0.02
41
+
42
+ # Maximum growth bonus (anti-fragile ceiling)
43
+ MAX_GROWTH_BONUS = 0.3
44
+
45
+ # Adversity severity levels
46
+ SEVERITY_LEVELS = {
47
+ minor: { impact: 0.1, recovery_ticks: 5 },
48
+ moderate: { impact: 0.3, recovery_ticks: 15 },
49
+ major: { impact: 0.5, recovery_ticks: 30 },
50
+ severe: { impact: 0.8, recovery_ticks: 60 },
51
+ critical: { impact: 1.0, recovery_ticks: 100 }
52
+ }.freeze
53
+
54
+ # Threshold for considering recovery complete
55
+ RECOVERY_THRESHOLD = 0.9
56
+
57
+ # Maximum active adversity events tracked
58
+ MAX_ACTIVE_ADVERSITIES = 20
59
+
60
+ # History cap
61
+ MAX_RESILIENCE_HISTORY = 200
62
+
63
+ # Fragility threshold — below this, the system is fragile
64
+ FRAGILITY_THRESHOLD = 0.3
65
+
66
+ # Anti-fragility threshold — above this, the system grows from stress
67
+ ANTIFRAGILITY_THRESHOLD = 0.7
68
+
69
+ # Consecutive recoveries needed to boost growth dimension
70
+ GROWTH_TRIGGER = 3
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Resilience
6
+ module Helpers
7
+ class ResilienceModel
8
+ attr_reader :dimensions, :growth_bonus, :history
9
+
10
+ def initialize
11
+ @dimensions = Constants::DIMENSIONS.keys.to_h { |d| [d, 0.5] }
12
+ @growth_bonus = 0.0
13
+ @history = []
14
+ end
15
+
16
+ def update_from_tracker(tracker)
17
+ update_elasticity(tracker)
18
+ update_robustness(tracker)
19
+ update_adaptability(tracker)
20
+ update_growth(tracker)
21
+
22
+ record_snapshot
23
+ end
24
+
25
+ def composite_score
26
+ total = 0.0
27
+ Constants::DIMENSIONS.each do |dim, config|
28
+ total += (@dimensions[dim] + (dim == :growth ? @growth_bonus : 0.0)) * config[:weight]
29
+ end
30
+ total.clamp(0.0, 1.0)
31
+ end
32
+
33
+ def classification
34
+ score = composite_score
35
+ if score >= Constants::ANTIFRAGILITY_THRESHOLD
36
+ :antifragile
37
+ elsif score >= 0.5
38
+ :resilient
39
+ elsif score >= Constants::FRAGILITY_THRESHOLD
40
+ :fragile
41
+ else
42
+ :brittle
43
+ end
44
+ end
45
+
46
+ def dimension_detail(name)
47
+ return nil unless @dimensions.key?(name)
48
+
49
+ {
50
+ name: name,
51
+ value: @dimensions[name].round(4),
52
+ config: Constants::DIMENSIONS[name],
53
+ trend: dimension_trend(name),
54
+ healthy: @dimensions[name] >= 0.5
55
+ }
56
+ end
57
+
58
+ def trend
59
+ return :insufficient_data if @history.size < 5
60
+
61
+ recent = @history.last(10)
62
+ scores = recent.map { |h| h[:composite] }
63
+ first_half = scores[0...(scores.size / 2)]
64
+ second_half = scores[(scores.size / 2)..]
65
+ diff = mean(second_half) - mean(first_half)
66
+
67
+ if diff > 0.03
68
+ :strengthening
69
+ elsif diff < -0.03
70
+ :weakening
71
+ else
72
+ :stable
73
+ end
74
+ end
75
+
76
+ def to_h
77
+ {
78
+ dimensions: @dimensions.transform_values { |v| v.round(4) },
79
+ growth_bonus: @growth_bonus.round(4),
80
+ composite: composite_score.round(4),
81
+ class: classification,
82
+ trend: trend
83
+ }
84
+ end
85
+
86
+ private
87
+
88
+ def update_elasticity(tracker)
89
+ speed = tracker.average_recovery_speed
90
+ signal = if speed.zero?
91
+ 0.5
92
+ elsif speed <= 1.0
93
+ 0.7 + ((1.0 - speed) * 0.3)
94
+ else
95
+ [0.3, 0.7 - ((speed - 1.0) * 0.2)].max
96
+ end
97
+ @dimensions[:elasticity] = ema(@dimensions[:elasticity], signal, Constants::RESILIENCE_ALPHA)
98
+ end
99
+
100
+ def update_robustness(tracker)
101
+ worst = tracker.worst_health
102
+ @dimensions[:robustness] = ema(@dimensions[:robustness], worst, Constants::RESILIENCE_ALPHA)
103
+ end
104
+
105
+ def update_adaptability(tracker)
106
+ rate = tracker.recovery_rate
107
+ signal = tracker.total_adversities.zero? ? 0.5 : rate
108
+ @dimensions[:adaptability] = ema(@dimensions[:adaptability], signal, Constants::RESILIENCE_ALPHA)
109
+ end
110
+
111
+ def update_growth(tracker)
112
+ if tracker.consecutive_recoveries >= Constants::GROWTH_TRIGGER
113
+ @growth_bonus = [@growth_bonus + Constants::GROWTH_INCREMENT, Constants::MAX_GROWTH_BONUS].min
114
+ end
115
+
116
+ growth_signal = tracker.total_adversities.zero? ? 0.5 : 0.5 + @growth_bonus
117
+ @dimensions[:growth] = ema(@dimensions[:growth], growth_signal, Constants::RESILIENCE_ALPHA)
118
+ end
119
+
120
+ def dimension_trend(name)
121
+ return :insufficient_data if @history.size < 5
122
+
123
+ recent = @history.last(10)
124
+ values = recent.map { |h| h[:dimensions][name] }
125
+ first_half = values[0...(values.size / 2)]
126
+ second_half = values[(values.size / 2)..]
127
+ diff = mean(second_half) - mean(first_half)
128
+
129
+ if diff > 0.02
130
+ :improving
131
+ elsif diff < -0.02
132
+ :declining
133
+ else
134
+ :stable
135
+ end
136
+ end
137
+
138
+ def ema(current, observed, alpha)
139
+ (current * (1.0 - alpha)) + (observed * alpha)
140
+ end
141
+
142
+ def mean(values)
143
+ return 0.0 if values.empty?
144
+
145
+ values.sum / values.size.to_f
146
+ end
147
+
148
+ def record_snapshot
149
+ @history << {
150
+ dimensions: @dimensions.dup,
151
+ composite: composite_score,
152
+ class: classification,
153
+ at: Time.now.utc
154
+ }
155
+ @history.shift while @history.size > Constants::MAX_RESILIENCE_HISTORY
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Resilience
6
+ module Runners
7
+ module Resilience
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def update_resilience(tick_results: {}, **)
12
+ detect_adversities(tick_results)
13
+ recovery = adversity_tracker.tick_recovery
14
+ resilience_model.update_from_tracker(adversity_tracker)
15
+
16
+ Legion::Logging.debug "[resilience] active=#{recovery[:active_count]} " \
17
+ "resolved=#{recovery[:resolved_count]} " \
18
+ "composite=#{resilience_model.composite_score.round(3)}"
19
+
20
+ {
21
+ active_adversities: recovery[:active_count],
22
+ resolved_this_tick: recovery[:resolved_count],
23
+ worst_health: recovery[:worst_health],
24
+ composite_score: resilience_model.composite_score.round(4),
25
+ classification: resilience_model.classification,
26
+ growth_bonus: resilience_model.growth_bonus.round(4)
27
+ }
28
+ end
29
+
30
+ def register_adversity(type:, severity:, context: {}, **)
31
+ adversity = adversity_tracker.register(type: type, severity: severity, context: context)
32
+ return { success: false, error: 'invalid type or severity' } unless adversity
33
+
34
+ Legion::Logging.info "[resilience] adversity registered: type=#{type} severity=#{severity}"
35
+ { success: true, adversity: adversity }
36
+ end
37
+
38
+ def resilience_status(**)
39
+ model_state = resilience_model.to_h
40
+ Legion::Logging.debug "[resilience] status: #{model_state[:class]} score=#{model_state[:composite]}"
41
+
42
+ model_state.merge(
43
+ active_adversities: adversity_tracker.active_adversities.size,
44
+ total_adversities: adversity_tracker.total_adversities,
45
+ consecutive_recoveries: adversity_tracker.consecutive_recoveries,
46
+ recovery_rate: adversity_tracker.recovery_rate.round(4)
47
+ )
48
+ end
49
+
50
+ def adversity_report(**)
51
+ Legion::Logging.debug '[resilience] adversity report'
52
+
53
+ {
54
+ active: adversity_tracker.active_adversities,
55
+ by_type: adversity_tracker.active_by_type,
56
+ total: adversity_tracker.total_adversities,
57
+ worst: adversity_tracker.worst_health.round(4),
58
+ avg_speed: adversity_tracker.average_recovery_speed.round(4)
59
+ }
60
+ end
61
+
62
+ def dimension_detail(dimension:, **)
63
+ detail = resilience_model.dimension_detail(dimension.to_sym)
64
+ return { error: "unknown dimension: #{dimension}" } unless detail
65
+
66
+ Legion::Logging.debug "[resilience] dimension #{dimension}: #{detail[:value]}"
67
+ detail
68
+ end
69
+
70
+ def resilience_stats(**)
71
+ Legion::Logging.debug '[resilience] stats'
72
+
73
+ {
74
+ composite: resilience_model.composite_score.round(4),
75
+ classification: resilience_model.classification,
76
+ dimensions: resilience_model.dimensions.transform_values { |v| v.round(4) },
77
+ growth_bonus: resilience_model.growth_bonus.round(4),
78
+ trend: resilience_model.trend,
79
+ total_adversities: adversity_tracker.total_adversities,
80
+ active_adversities: adversity_tracker.active_adversities.size,
81
+ recovery_rate: adversity_tracker.recovery_rate.round(4),
82
+ consecutive_recoveries: adversity_tracker.consecutive_recoveries,
83
+ history_size: resilience_model.history.size
84
+ }
85
+ end
86
+
87
+ private
88
+
89
+ def adversity_tracker
90
+ @adversity_tracker ||= Helpers::AdversityTracker.new
91
+ end
92
+
93
+ def resilience_model
94
+ @resilience_model ||= Helpers::ResilienceModel.new
95
+ end
96
+
97
+ def detect_adversities(tick_results)
98
+ detect_prediction_adversity(tick_results)
99
+ detect_trust_adversity(tick_results)
100
+ detect_conflict_adversity(tick_results)
101
+ detect_energy_adversity(tick_results)
102
+ detect_emotional_adversity(tick_results)
103
+ end
104
+
105
+ def detect_prediction_adversity(tick_results)
106
+ error_rate = tick_results.dig(:prediction_engine, :error_rate)
107
+ return unless error_rate && error_rate > 0.7
108
+
109
+ severity = error_rate > 0.9 ? :major : :moderate
110
+ adversity_tracker.register(type: :prediction_failure, severity: severity)
111
+ end
112
+
113
+ def detect_trust_adversity(tick_results)
114
+ violation = tick_results.dig(:trust, :violation)
115
+ return unless violation
116
+
117
+ adversity_tracker.register(type: :trust_violation, severity: :major)
118
+ end
119
+
120
+ def detect_conflict_adversity(tick_results)
121
+ conflict_severity = tick_results.dig(:conflict, :severity)
122
+ return unless conflict_severity && conflict_severity >= 3
123
+
124
+ severity = conflict_severity >= 4 ? :severe : :moderate
125
+ adversity_tracker.register(type: :conflict_escalation, severity: severity)
126
+ end
127
+
128
+ def detect_energy_adversity(tick_results)
129
+ energy = tick_results.dig(:fatigue, :energy)
130
+ return unless energy && energy < 0.2
131
+
132
+ severity = energy < 0.1 ? :major : :moderate
133
+ adversity_tracker.register(type: :resource_depletion, severity: severity)
134
+ end
135
+
136
+ def detect_emotional_adversity(tick_results)
137
+ arousal = tick_results.dig(:emotional_evaluation, :arousal)
138
+ return unless arousal && arousal > 0.9
139
+
140
+ adversity_tracker.register(type: :emotional_shock, severity: :moderate)
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Resilience
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/resilience/version'
4
+ require 'legion/extensions/resilience/helpers/constants'
5
+ require 'legion/extensions/resilience/helpers/adversity_tracker'
6
+ require 'legion/extensions/resilience/helpers/resilience_model'
7
+ require 'legion/extensions/resilience/runners/resilience'
8
+ require 'legion/extensions/resilience/client'
9
+
10
+ module Legion
11
+ module Extensions
12
+ module Resilience
13
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined?(:Core)
14
+ end
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-resilience
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matthew Iverson
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: legion-gaia
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ description: Models resilience as the capacity to recover from setbacks and grow stronger
27
+ from adversity
28
+ email:
29
+ - matt@legionIO.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - lib/legion/extensions/resilience.rb
35
+ - lib/legion/extensions/resilience/client.rb
36
+ - lib/legion/extensions/resilience/helpers/adversity_tracker.rb
37
+ - lib/legion/extensions/resilience/helpers/constants.rb
38
+ - lib/legion/extensions/resilience/helpers/resilience_model.rb
39
+ - lib/legion/extensions/resilience/runners/resilience.rb
40
+ - lib/legion/extensions/resilience/version.rb
41
+ homepage: https://github.com/LegionIO/lex-resilience
42
+ licenses:
43
+ - MIT
44
+ metadata:
45
+ rubygems_mfa_required: 'true'
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '3.4'
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubygems_version: 3.6.9
61
+ specification_version: 4
62
+ summary: Anti-fragility and adversity recovery for LegionIO cognitive agents
63
+ test_files: []