lex-agentic-affect 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.
Files changed (218) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +12 -0
  3. data/Gemfile +5 -0
  4. data/LICENSE +21 -0
  5. data/README.md +13 -0
  6. data/lex-agentic-affect.gemspec +30 -0
  7. data/lib/legion/extensions/agentic/affect/appraisal/client.rb +20 -0
  8. data/lib/legion/extensions/agentic/affect/appraisal/helpers/appraisal.rb +112 -0
  9. data/lib/legion/extensions/agentic/affect/appraisal/helpers/appraisal_engine.rb +129 -0
  10. data/lib/legion/extensions/agentic/affect/appraisal/helpers/constants.rb +43 -0
  11. data/lib/legion/extensions/agentic/affect/appraisal/runners/appraisal.rb +105 -0
  12. data/lib/legion/extensions/agentic/affect/appraisal/version.rb +13 -0
  13. data/lib/legion/extensions/agentic/affect/appraisal.rb +19 -0
  14. data/lib/legion/extensions/agentic/affect/cognitive_empathy/client.rb +19 -0
  15. data/lib/legion/extensions/agentic/affect/cognitive_empathy/helpers/constants.rb +37 -0
  16. data/lib/legion/extensions/agentic/affect/cognitive_empathy/helpers/empathy_engine.rb +151 -0
  17. data/lib/legion/extensions/agentic/affect/cognitive_empathy/helpers/perspective.rb +92 -0
  18. data/lib/legion/extensions/agentic/affect/cognitive_empathy/runners/cognitive_empathy.rb +93 -0
  19. data/lib/legion/extensions/agentic/affect/cognitive_empathy/version.rb +13 -0
  20. data/lib/legion/extensions/agentic/affect/cognitive_empathy.rb +20 -0
  21. data/lib/legion/extensions/agentic/affect/contagion/client.rb +28 -0
  22. data/lib/legion/extensions/agentic/affect/contagion/helpers/constants.rb +34 -0
  23. data/lib/legion/extensions/agentic/affect/contagion/helpers/contagion_engine.rb +184 -0
  24. data/lib/legion/extensions/agentic/affect/contagion/helpers/meme.rb +97 -0
  25. data/lib/legion/extensions/agentic/affect/contagion/runners/cognitive_contagion.rb +125 -0
  26. data/lib/legion/extensions/agentic/affect/contagion/version.rb +13 -0
  27. data/lib/legion/extensions/agentic/affect/contagion.rb +19 -0
  28. data/lib/legion/extensions/agentic/affect/defusion/client.rb +28 -0
  29. data/lib/legion/extensions/agentic/affect/defusion/helpers/constants.rb +64 -0
  30. data/lib/legion/extensions/agentic/affect/defusion/helpers/defusion_engine.rb +167 -0
  31. data/lib/legion/extensions/agentic/affect/defusion/helpers/thought.rb +92 -0
  32. data/lib/legion/extensions/agentic/affect/defusion/runners/cognitive_defusion.rb +127 -0
  33. data/lib/legion/extensions/agentic/affect/defusion/version.rb +13 -0
  34. data/lib/legion/extensions/agentic/affect/defusion.rb +19 -0
  35. data/lib/legion/extensions/agentic/affect/emotion/actors/momentum_decay.rb +45 -0
  36. data/lib/legion/extensions/agentic/affect/emotion/client.rb +36 -0
  37. data/lib/legion/extensions/agentic/affect/emotion/helpers/baseline.rb +52 -0
  38. data/lib/legion/extensions/agentic/affect/emotion/helpers/momentum.rb +52 -0
  39. data/lib/legion/extensions/agentic/affect/emotion/helpers/valence.rb +92 -0
  40. data/lib/legion/extensions/agentic/affect/emotion/runners/gut.rb +102 -0
  41. data/lib/legion/extensions/agentic/affect/emotion/runners/valence.rb +120 -0
  42. data/lib/legion/extensions/agentic/affect/emotion/version.rb +13 -0
  43. data/lib/legion/extensions/agentic/affect/emotion.rb +20 -0
  44. data/lib/legion/extensions/agentic/affect/empathy/client.rb +21 -0
  45. data/lib/legion/extensions/agentic/affect/empathy/helpers/constants.rb +54 -0
  46. data/lib/legion/extensions/agentic/affect/empathy/helpers/mental_model.rb +185 -0
  47. data/lib/legion/extensions/agentic/affect/empathy/helpers/model_store.rb +88 -0
  48. data/lib/legion/extensions/agentic/affect/empathy/runners/empathy.rb +173 -0
  49. data/lib/legion/extensions/agentic/affect/empathy/version.rb +13 -0
  50. data/lib/legion/extensions/agentic/affect/empathy.rb +20 -0
  51. data/lib/legion/extensions/agentic/affect/fatigue/client.rb +26 -0
  52. data/lib/legion/extensions/agentic/affect/fatigue/helpers/constants.rb +54 -0
  53. data/lib/legion/extensions/agentic/affect/fatigue/helpers/energy_model.rb +181 -0
  54. data/lib/legion/extensions/agentic/affect/fatigue/helpers/fatigue_store.rb +146 -0
  55. data/lib/legion/extensions/agentic/affect/fatigue/runners/fatigue.rb +89 -0
  56. data/lib/legion/extensions/agentic/affect/fatigue/version.rb +13 -0
  57. data/lib/legion/extensions/agentic/affect/fatigue.rb +19 -0
  58. data/lib/legion/extensions/agentic/affect/flow/client.rb +25 -0
  59. data/lib/legion/extensions/agentic/affect/flow/helpers/constants.rb +84 -0
  60. data/lib/legion/extensions/agentic/affect/flow/helpers/flow_detector.rb +166 -0
  61. data/lib/legion/extensions/agentic/affect/flow/runners/flow.rb +129 -0
  62. data/lib/legion/extensions/agentic/affect/flow/version.rb +13 -0
  63. data/lib/legion/extensions/agentic/affect/flow.rb +18 -0
  64. data/lib/legion/extensions/agentic/affect/interoception/actors/decay.rb +45 -0
  65. data/lib/legion/extensions/agentic/affect/interoception/client.rb +28 -0
  66. data/lib/legion/extensions/agentic/affect/interoception/helpers/body_budget.rb +152 -0
  67. data/lib/legion/extensions/agentic/affect/interoception/helpers/constants.rb +68 -0
  68. data/lib/legion/extensions/agentic/affect/interoception/helpers/somatic_marker.rb +75 -0
  69. data/lib/legion/extensions/agentic/affect/interoception/runners/interoception.rb +101 -0
  70. data/lib/legion/extensions/agentic/affect/interoception/version.rb +13 -0
  71. data/lib/legion/extensions/agentic/affect/interoception.rb +20 -0
  72. data/lib/legion/extensions/agentic/affect/mood/client.rb +21 -0
  73. data/lib/legion/extensions/agentic/affect/mood/helpers/constants.rb +78 -0
  74. data/lib/legion/extensions/agentic/affect/mood/helpers/mood_state.rb +154 -0
  75. data/lib/legion/extensions/agentic/affect/mood/runners/mood.rb +122 -0
  76. data/lib/legion/extensions/agentic/affect/mood/version.rb +13 -0
  77. data/lib/legion/extensions/agentic/affect/mood.rb +18 -0
  78. data/lib/legion/extensions/agentic/affect/motivation/client.rb +26 -0
  79. data/lib/legion/extensions/agentic/affect/motivation/helpers/constants.rb +48 -0
  80. data/lib/legion/extensions/agentic/affect/motivation/helpers/drive_state.rb +98 -0
  81. data/lib/legion/extensions/agentic/affect/motivation/helpers/motivation_store.rb +106 -0
  82. data/lib/legion/extensions/agentic/affect/motivation/runners/motivation.rb +165 -0
  83. data/lib/legion/extensions/agentic/affect/motivation/version.rb +13 -0
  84. data/lib/legion/extensions/agentic/affect/motivation.rb +19 -0
  85. data/lib/legion/extensions/agentic/affect/reappraisal/actors/auto_regulate.rb +45 -0
  86. data/lib/legion/extensions/agentic/affect/reappraisal/client.rb +28 -0
  87. data/lib/legion/extensions/agentic/affect/reappraisal/helpers/constants.rb +82 -0
  88. data/lib/legion/extensions/agentic/affect/reappraisal/helpers/emotional_event.rb +98 -0
  89. data/lib/legion/extensions/agentic/affect/reappraisal/helpers/llm_enhancer.rb +88 -0
  90. data/lib/legion/extensions/agentic/affect/reappraisal/helpers/reappraisal_engine.rb +153 -0
  91. data/lib/legion/extensions/agentic/affect/reappraisal/runners/cognitive_reappraisal.rb +164 -0
  92. data/lib/legion/extensions/agentic/affect/reappraisal/version.rb +13 -0
  93. data/lib/legion/extensions/agentic/affect/reappraisal.rb +20 -0
  94. data/lib/legion/extensions/agentic/affect/regulation/client.rb +25 -0
  95. data/lib/legion/extensions/agentic/affect/regulation/helpers/constants.rb +71 -0
  96. data/lib/legion/extensions/agentic/affect/regulation/helpers/regulation_model.rb +175 -0
  97. data/lib/legion/extensions/agentic/affect/regulation/runners/emotional_regulation.rb +127 -0
  98. data/lib/legion/extensions/agentic/affect/regulation/version.rb +13 -0
  99. data/lib/legion/extensions/agentic/affect/regulation.rb +18 -0
  100. data/lib/legion/extensions/agentic/affect/resilience/client.rb +27 -0
  101. data/lib/legion/extensions/agentic/affect/resilience/helpers/adversity_tracker.rb +130 -0
  102. data/lib/legion/extensions/agentic/affect/resilience/helpers/constants.rb +79 -0
  103. data/lib/legion/extensions/agentic/affect/resilience/helpers/resilience_model.rb +165 -0
  104. data/lib/legion/extensions/agentic/affect/resilience/runners/resilience.rb +150 -0
  105. data/lib/legion/extensions/agentic/affect/resilience/version.rb +13 -0
  106. data/lib/legion/extensions/agentic/affect/resilience.rb +19 -0
  107. data/lib/legion/extensions/agentic/affect/resonance/client.rb +24 -0
  108. data/lib/legion/extensions/agentic/affect/resonance/helpers/category.rb +75 -0
  109. data/lib/legion/extensions/agentic/affect/resonance/helpers/constants.rb +47 -0
  110. data/lib/legion/extensions/agentic/affect/resonance/helpers/resonance_engine.rb +115 -0
  111. data/lib/legion/extensions/agentic/affect/resonance/runners/cognitive_resonance.rb +94 -0
  112. data/lib/legion/extensions/agentic/affect/resonance/version.rb +13 -0
  113. data/lib/legion/extensions/agentic/affect/resonance.rb +19 -0
  114. data/lib/legion/extensions/agentic/affect/reward/client.rb +26 -0
  115. data/lib/legion/extensions/agentic/affect/reward/helpers/constants.rb +67 -0
  116. data/lib/legion/extensions/agentic/affect/reward/helpers/reward_signal.rb +178 -0
  117. data/lib/legion/extensions/agentic/affect/reward/helpers/reward_store.rb +142 -0
  118. data/lib/legion/extensions/agentic/affect/reward/runners/reward.rb +92 -0
  119. data/lib/legion/extensions/agentic/affect/reward/version.rb +13 -0
  120. data/lib/legion/extensions/agentic/affect/reward.rb +19 -0
  121. data/lib/legion/extensions/agentic/affect/somatic_marker/actors/decay.rb +45 -0
  122. data/lib/legion/extensions/agentic/affect/somatic_marker/client.rb +29 -0
  123. data/lib/legion/extensions/agentic/affect/somatic_marker/helpers/body_state.rb +69 -0
  124. data/lib/legion/extensions/agentic/affect/somatic_marker/helpers/constants.rb +43 -0
  125. data/lib/legion/extensions/agentic/affect/somatic_marker/helpers/marker_store.rb +160 -0
  126. data/lib/legion/extensions/agentic/affect/somatic_marker/helpers/somatic_marker.rb +74 -0
  127. data/lib/legion/extensions/agentic/affect/somatic_marker/runners/somatic_marker.rb +132 -0
  128. data/lib/legion/extensions/agentic/affect/somatic_marker/version.rb +13 -0
  129. data/lib/legion/extensions/agentic/affect/somatic_marker.rb +20 -0
  130. data/lib/legion/extensions/agentic/affect/version.rb +11 -0
  131. data/lib/legion/extensions/agentic/affect.rb +34 -0
  132. data/spec/legion/extensions/agentic/affect/appraisal/client_spec.rb +52 -0
  133. data/spec/legion/extensions/agentic/affect/appraisal/helpers/appraisal_engine_spec.rb +161 -0
  134. data/spec/legion/extensions/agentic/affect/appraisal/helpers/appraisal_spec.rb +175 -0
  135. data/spec/legion/extensions/agentic/affect/appraisal/helpers/constants_spec.rb +49 -0
  136. data/spec/legion/extensions/agentic/affect/appraisal/runners/appraisal_spec.rb +116 -0
  137. data/spec/legion/extensions/agentic/affect/cognitive_empathy/client_spec.rb +62 -0
  138. data/spec/legion/extensions/agentic/affect/cognitive_empathy/helpers/empathy_engine_spec.rb +316 -0
  139. data/spec/legion/extensions/agentic/affect/cognitive_empathy/helpers/perspective_spec.rb +132 -0
  140. data/spec/legion/extensions/agentic/affect/cognitive_empathy/runners/cognitive_empathy_spec.rb +200 -0
  141. data/spec/legion/extensions/agentic/affect/contagion/client_spec.rb +63 -0
  142. data/spec/legion/extensions/agentic/affect/contagion/helpers/constants_spec.rb +86 -0
  143. data/spec/legion/extensions/agentic/affect/contagion/helpers/contagion_engine_spec.rb +241 -0
  144. data/spec/legion/extensions/agentic/affect/contagion/helpers/meme_spec.rb +160 -0
  145. data/spec/legion/extensions/agentic/affect/contagion/runners/cognitive_contagion_spec.rb +211 -0
  146. data/spec/legion/extensions/agentic/affect/defusion/client_spec.rb +80 -0
  147. data/spec/legion/extensions/agentic/affect/defusion/helpers/constants_spec.rb +84 -0
  148. data/spec/legion/extensions/agentic/affect/defusion/helpers/defusion_engine_spec.rb +250 -0
  149. data/spec/legion/extensions/agentic/affect/defusion/helpers/thought_spec.rb +178 -0
  150. data/spec/legion/extensions/agentic/affect/defusion/runners/cognitive_defusion_spec.rb +185 -0
  151. data/spec/legion/extensions/agentic/affect/emotion/actors/momentum_decay_spec.rb +46 -0
  152. data/spec/legion/extensions/agentic/affect/emotion/client_spec.rb +46 -0
  153. data/spec/legion/extensions/agentic/affect/emotion/helpers/baseline_spec.rb +48 -0
  154. data/spec/legion/extensions/agentic/affect/emotion/helpers/momentum_spec.rb +45 -0
  155. data/spec/legion/extensions/agentic/affect/emotion/helpers/valence_spec.rb +91 -0
  156. data/spec/legion/extensions/agentic/affect/emotion/runners/gut_spec.rb +73 -0
  157. data/spec/legion/extensions/agentic/affect/emotion/runners/valence_spec.rb +67 -0
  158. data/spec/legion/extensions/agentic/affect/empathy/client_spec.rb +20 -0
  159. data/spec/legion/extensions/agentic/affect/empathy/helpers/constants_spec.rb +23 -0
  160. data/spec/legion/extensions/agentic/affect/empathy/helpers/mental_model_spec.rb +150 -0
  161. data/spec/legion/extensions/agentic/affect/empathy/helpers/model_store_spec.rb +94 -0
  162. data/spec/legion/extensions/agentic/affect/empathy/runners/empathy_spec.rb +127 -0
  163. data/spec/legion/extensions/agentic/affect/fatigue/client_spec.rb +66 -0
  164. data/spec/legion/extensions/agentic/affect/fatigue/helpers/constants_spec.rb +130 -0
  165. data/spec/legion/extensions/agentic/affect/fatigue/helpers/energy_model_spec.rb +281 -0
  166. data/spec/legion/extensions/agentic/affect/fatigue/helpers/fatigue_store_spec.rb +157 -0
  167. data/spec/legion/extensions/agentic/affect/fatigue/runners/fatigue_spec.rb +127 -0
  168. data/spec/legion/extensions/agentic/affect/flow/client_spec.rb +58 -0
  169. data/spec/legion/extensions/agentic/affect/flow/helpers/constants_spec.rb +112 -0
  170. data/spec/legion/extensions/agentic/affect/flow/helpers/flow_detector_spec.rb +268 -0
  171. data/spec/legion/extensions/agentic/affect/flow/runners/flow_spec.rb +222 -0
  172. data/spec/legion/extensions/agentic/affect/interoception/client_spec.rb +52 -0
  173. data/spec/legion/extensions/agentic/affect/interoception/helpers/body_budget_spec.rb +178 -0
  174. data/spec/legion/extensions/agentic/affect/interoception/helpers/somatic_marker_spec.rb +120 -0
  175. data/spec/legion/extensions/agentic/affect/interoception/runners/interoception_spec.rb +108 -0
  176. data/spec/legion/extensions/agentic/affect/mood/client_spec.rb +20 -0
  177. data/spec/legion/extensions/agentic/affect/mood/helpers/constants_spec.rb +29 -0
  178. data/spec/legion/extensions/agentic/affect/mood/helpers/mood_state_spec.rb +94 -0
  179. data/spec/legion/extensions/agentic/affect/mood/runners/mood_spec.rb +71 -0
  180. data/spec/legion/extensions/agentic/affect/motivation/client_spec.rb +35 -0
  181. data/spec/legion/extensions/agentic/affect/motivation/helpers/constants_spec.rb +111 -0
  182. data/spec/legion/extensions/agentic/affect/motivation/helpers/drive_state_spec.rb +183 -0
  183. data/spec/legion/extensions/agentic/affect/motivation/helpers/motivation_store_spec.rb +185 -0
  184. data/spec/legion/extensions/agentic/affect/motivation/runners/motivation_spec.rb +248 -0
  185. data/spec/legion/extensions/agentic/affect/reappraisal/actors/auto_regulate_spec.rb +46 -0
  186. data/spec/legion/extensions/agentic/affect/reappraisal/client_spec.rb +64 -0
  187. data/spec/legion/extensions/agentic/affect/reappraisal/helpers/constants_spec.rb +102 -0
  188. data/spec/legion/extensions/agentic/affect/reappraisal/helpers/emotional_event_spec.rb +177 -0
  189. data/spec/legion/extensions/agentic/affect/reappraisal/helpers/llm_enhancer_spec.rb +161 -0
  190. data/spec/legion/extensions/agentic/affect/reappraisal/helpers/reappraisal_engine_spec.rb +211 -0
  191. data/spec/legion/extensions/agentic/affect/reappraisal/runners/cognitive_reappraisal_spec.rb +312 -0
  192. data/spec/legion/extensions/agentic/affect/regulation/client_spec.rb +61 -0
  193. data/spec/legion/extensions/agentic/affect/regulation/helpers/constants_spec.rb +108 -0
  194. data/spec/legion/extensions/agentic/affect/regulation/helpers/regulation_model_spec.rb +200 -0
  195. data/spec/legion/extensions/agentic/affect/regulation/runners/emotional_regulation_spec.rb +190 -0
  196. data/spec/legion/extensions/agentic/affect/resilience/client_spec.rb +36 -0
  197. data/spec/legion/extensions/agentic/affect/resilience/helpers/adversity_tracker_spec.rb +164 -0
  198. data/spec/legion/extensions/agentic/affect/resilience/helpers/constants_spec.rb +78 -0
  199. data/spec/legion/extensions/agentic/affect/resilience/helpers/resilience_model_spec.rb +133 -0
  200. data/spec/legion/extensions/agentic/affect/resilience/runners/resilience_spec.rb +150 -0
  201. data/spec/legion/extensions/agentic/affect/resonance/client_spec.rb +66 -0
  202. data/spec/legion/extensions/agentic/affect/resonance/cognitive_resonance_spec.rb +27 -0
  203. data/spec/legion/extensions/agentic/affect/resonance/helpers/category_spec.rb +146 -0
  204. data/spec/legion/extensions/agentic/affect/resonance/helpers/constants_spec.rb +104 -0
  205. data/spec/legion/extensions/agentic/affect/resonance/helpers/resonance_engine_spec.rb +189 -0
  206. data/spec/legion/extensions/agentic/affect/resonance/runners/cognitive_resonance_spec.rb +197 -0
  207. data/spec/legion/extensions/agentic/affect/reward/client_spec.rb +42 -0
  208. data/spec/legion/extensions/agentic/affect/reward/helpers/constants_spec.rb +91 -0
  209. data/spec/legion/extensions/agentic/affect/reward/helpers/reward_signal_spec.rb +296 -0
  210. data/spec/legion/extensions/agentic/affect/reward/helpers/reward_store_spec.rb +167 -0
  211. data/spec/legion/extensions/agentic/affect/reward/runners/reward_spec.rb +149 -0
  212. data/spec/legion/extensions/agentic/affect/somatic_marker/client_spec.rb +83 -0
  213. data/spec/legion/extensions/agentic/affect/somatic_marker/helpers/body_state_spec.rb +155 -0
  214. data/spec/legion/extensions/agentic/affect/somatic_marker/helpers/marker_store_spec.rb +233 -0
  215. data/spec/legion/extensions/agentic/affect/somatic_marker/helpers/somatic_marker_spec.rb +172 -0
  216. data/spec/legion/extensions/agentic/affect/somatic_marker/runners/somatic_marker_spec.rb +181 -0
  217. data/spec/spec_helper.rb +46 -0
  218. metadata +302 -0
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Legion::Extensions::Agentic::Affect::Reward::Helpers::Constants do
6
+ describe 'REWARD_SOURCES' do
7
+ it 'defines 8 sources' do
8
+ expect(described_class::REWARD_SOURCES.size).to eq(8)
9
+ end
10
+
11
+ it 'has weights summing to 1.0' do
12
+ total = described_class::REWARD_SOURCES.values.sum { |v| v[:weight] }
13
+ expect(total).to be_within(0.001).of(1.0)
14
+ end
15
+
16
+ it 'includes prediction accuracy' do
17
+ expect(described_class::REWARD_SOURCES).to have_key(:prediction_accuracy)
18
+ end
19
+
20
+ it 'includes curiosity resolved' do
21
+ expect(described_class::REWARD_SOURCES).to have_key(:curiosity_resolved)
22
+ end
23
+
24
+ it 'includes goal achieved' do
25
+ expect(described_class::REWARD_SOURCES).to have_key(:goal_achieved)
26
+ end
27
+
28
+ it 'is frozen' do
29
+ expect(described_class::REWARD_SOURCES).to be_frozen
30
+ end
31
+ end
32
+
33
+ describe 'REWARD_ALPHA' do
34
+ it 'is 0.15' do
35
+ expect(described_class::REWARD_ALPHA).to eq(0.15)
36
+ end
37
+ end
38
+
39
+ describe 'PREDICTION_ALPHA' do
40
+ it 'is 0.1' do
41
+ expect(described_class::PREDICTION_ALPHA).to eq(0.1)
42
+ end
43
+ end
44
+
45
+ describe 'RPE_THRESHOLD' do
46
+ it 'is 0.05' do
47
+ expect(described_class::RPE_THRESHOLD).to eq(0.05)
48
+ end
49
+ end
50
+
51
+ describe 'REWARD_RANGE' do
52
+ it 'spans -1.0 to 1.0' do
53
+ expect(described_class::REWARD_RANGE[:min]).to eq(-1.0)
54
+ expect(described_class::REWARD_RANGE[:max]).to eq(1.0)
55
+ end
56
+ end
57
+
58
+ describe 'RPE_LEVELS' do
59
+ it 'defines 5 levels' do
60
+ expect(described_class::RPE_LEVELS.size).to eq(5)
61
+ end
62
+
63
+ it 'has large_positive > positive > neutral thresholds' do
64
+ levels = described_class::RPE_LEVELS
65
+ expect(levels[:large_positive]).to be > levels[:positive]
66
+ expect(levels[:positive]).to be > levels[:neutral]
67
+ end
68
+ end
69
+
70
+ describe 'TEMPORAL_DISCOUNT' do
71
+ it 'is 0.95' do
72
+ expect(described_class::TEMPORAL_DISCOUNT).to eq(0.95)
73
+ end
74
+ end
75
+
76
+ describe 'thresholds' do
77
+ it 'defines anhedonia threshold' do
78
+ expect(described_class::ANHEDONIA_THRESHOLD).to eq(-0.3)
79
+ end
80
+
81
+ it 'defines euphoria threshold' do
82
+ expect(described_class::EUPHORIA_THRESHOLD).to eq(0.7)
83
+ end
84
+ end
85
+
86
+ describe 'MAX_REWARD_HISTORY' do
87
+ it 'caps at 200' do
88
+ expect(described_class::MAX_REWARD_HISTORY).to eq(200)
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,296 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Legion::Extensions::Agentic::Affect::Reward::Helpers::RewardSignal do
6
+ subject(:signal) { described_class.new }
7
+
8
+ let(:positive_sources) do
9
+ {
10
+ prediction_accuracy: 0.8,
11
+ curiosity_resolved: 0.5,
12
+ goal_achieved: 0.6,
13
+ social_approval: 0.3,
14
+ flow_state: 0.4,
15
+ error_avoidance: 0.7,
16
+ novelty_encounter: 0.3,
17
+ homeostatic_balance: 0.2
18
+ }
19
+ end
20
+
21
+ let(:negative_sources) do
22
+ {
23
+ prediction_accuracy: -0.5,
24
+ curiosity_resolved: -0.3,
25
+ goal_achieved: -0.8,
26
+ social_approval: -0.4,
27
+ flow_state: -0.2,
28
+ error_avoidance: -0.6,
29
+ novelty_encounter: -0.1,
30
+ homeostatic_balance: -0.3
31
+ }
32
+ end
33
+
34
+ let(:neutral_sources) do
35
+ {
36
+ prediction_accuracy: 0.0,
37
+ curiosity_resolved: 0.0,
38
+ goal_achieved: 0.0,
39
+ social_approval: 0.0,
40
+ flow_state: 0.0,
41
+ error_avoidance: 0.0,
42
+ novelty_encounter: 0.0,
43
+ homeostatic_balance: 0.0
44
+ }
45
+ end
46
+
47
+ describe '#initialize' do
48
+ it 'starts with zero running average' do
49
+ expect(signal.running_average).to eq(0.0)
50
+ end
51
+
52
+ it 'starts with zero predicted reward' do
53
+ expect(signal.predicted_reward).to eq(0.0)
54
+ end
55
+
56
+ it 'starts with zero RPE' do
57
+ expect(signal.last_rpe).to eq(0.0)
58
+ end
59
+
60
+ it 'starts with empty history' do
61
+ expect(signal.history).to be_empty
62
+ end
63
+
64
+ it 'starts with zero tick count' do
65
+ expect(signal.tick_count).to eq(0)
66
+ end
67
+ end
68
+
69
+ describe '#compute' do
70
+ it 'returns reward result hash' do
71
+ result = signal.compute(positive_sources)
72
+ expect(result).to include(:reward, :rpe, :rpe_class, :running_average,
73
+ :predicted_reward, :sources, :learning_signal)
74
+ end
75
+
76
+ it 'computes positive reward from positive sources' do
77
+ result = signal.compute(positive_sources)
78
+ expect(result[:reward]).to be > 0.0
79
+ end
80
+
81
+ it 'computes negative reward from negative sources' do
82
+ result = signal.compute(negative_sources)
83
+ expect(result[:reward]).to be < 0.0
84
+ end
85
+
86
+ it 'computes zero reward from neutral sources' do
87
+ result = signal.compute(neutral_sources)
88
+ expect(result[:reward]).to eq(0.0)
89
+ end
90
+
91
+ it 'clamps reward to [-1.0, 1.0]' do
92
+ extreme = positive_sources.transform_values { 10.0 }
93
+ result = signal.compute(extreme)
94
+ expect(result[:reward]).to be <= 1.0
95
+ end
96
+
97
+ it 'increments tick count' do
98
+ signal.compute(positive_sources)
99
+ expect(signal.tick_count).to eq(1)
100
+ end
101
+
102
+ it 'records in history' do
103
+ signal.compute(positive_sources)
104
+ expect(signal.history.size).to eq(1)
105
+ end
106
+
107
+ it 'computes RPE as actual minus predicted' do
108
+ signal.compute(neutral_sources)
109
+ result = signal.compute(positive_sources)
110
+ expect(result[:rpe]).to be > 0.0
111
+ end
112
+
113
+ it 'updates running average via EMA' do
114
+ signal.compute(positive_sources)
115
+ expect(signal.running_average).to be > 0.0
116
+ end
117
+
118
+ it 'updates predicted reward via EMA' do
119
+ signal.compute(positive_sources)
120
+ expect(signal.predicted_reward).to be > 0.0
121
+ end
122
+ end
123
+
124
+ describe 'RPE classification' do
125
+ it 'classifies large positive RPE' do
126
+ # First tick with neutral, then large positive
127
+ signal.compute(neutral_sources)
128
+ result = signal.compute(positive_sources)
129
+ expect(result[:rpe_class]).to be_a(Symbol)
130
+ end
131
+
132
+ it 'classifies neutral RPE for stable rewards' do
133
+ 20.times { signal.compute(neutral_sources) }
134
+ result = signal.compute(neutral_sources)
135
+ expect(result[:rpe_class]).to eq(:neutral)
136
+ end
137
+ end
138
+
139
+ describe '#record_domain_reward' do
140
+ it 'stores domain-specific rewards' do
141
+ signal.record_domain_reward(:networking, 0.5)
142
+ expect(signal.domain_history[:networking].size).to eq(1)
143
+ end
144
+
145
+ it 'caps domain history at MAX_DOMAIN_HISTORY' do
146
+ max = Legion::Extensions::Agentic::Affect::Reward::Helpers::Constants::MAX_DOMAIN_HISTORY
147
+ (max + 5).times { signal.record_domain_reward(:test, 0.1) }
148
+ expect(signal.domain_history[:test].size).to eq(max)
149
+ end
150
+ end
151
+
152
+ describe '#domain_average' do
153
+ it 'returns 0.0 for unknown domain' do
154
+ expect(signal.domain_average(:unknown)).to eq(0.0)
155
+ end
156
+
157
+ it 'computes average of domain rewards' do
158
+ signal.record_domain_reward(:test, 0.4)
159
+ signal.record_domain_reward(:test, 0.6)
160
+ expect(signal.domain_average(:test)).to eq(0.5)
161
+ end
162
+ end
163
+
164
+ describe '#domain_trend' do
165
+ it 'returns :no_data for unknown domain' do
166
+ expect(signal.domain_trend(:unknown)).to eq(:no_data)
167
+ end
168
+
169
+ it 'returns :no_data with insufficient entries' do
170
+ 3.times { signal.record_domain_reward(:test, 0.5) }
171
+ expect(signal.domain_trend(:test)).to eq(:no_data)
172
+ end
173
+
174
+ it 'detects improving trend' do
175
+ 5.times { signal.record_domain_reward(:test, 0.1) }
176
+ 5.times { signal.record_domain_reward(:test, 0.9) }
177
+ expect(signal.domain_trend(:test)).to eq(:improving)
178
+ end
179
+
180
+ it 'detects declining trend' do
181
+ 5.times { signal.record_domain_reward(:test, 0.9) }
182
+ 5.times { signal.record_domain_reward(:test, 0.1) }
183
+ expect(signal.domain_trend(:test)).to eq(:declining)
184
+ end
185
+
186
+ it 'detects stable trend' do
187
+ 10.times { signal.record_domain_reward(:test, 0.5) }
188
+ expect(signal.domain_trend(:test)).to eq(:stable)
189
+ end
190
+ end
191
+
192
+ describe '#anhedonic?' do
193
+ it 'returns false initially' do
194
+ expect(signal.anhedonic?).to be false
195
+ end
196
+
197
+ it 'returns true with persistent negative rewards' do
198
+ 50.times { signal.compute(negative_sources) }
199
+ expect(signal.anhedonic?).to be true
200
+ end
201
+ end
202
+
203
+ describe '#euphoric?' do
204
+ it 'returns false initially' do
205
+ expect(signal.euphoric?).to be false
206
+ end
207
+
208
+ it 'returns true with persistent high rewards' do
209
+ extreme_positive = positive_sources.transform_values { 1.0 }
210
+ 50.times { signal.compute(extreme_positive) }
211
+ expect(signal.euphoric?).to be true
212
+ end
213
+ end
214
+
215
+ describe '#learning_signal?' do
216
+ it 'returns false when RPE is below threshold' do
217
+ 20.times { signal.compute(neutral_sources) }
218
+ expect(signal.learning_signal?).to be false
219
+ end
220
+
221
+ it 'returns true when RPE exceeds threshold' do
222
+ signal.compute(neutral_sources)
223
+ signal.compute(positive_sources)
224
+ expect(signal.learning_signal?).to be true
225
+ end
226
+ end
227
+
228
+ describe '#recent_rewards' do
229
+ it 'returns empty for no history' do
230
+ expect(signal.recent_rewards).to be_empty
231
+ end
232
+
233
+ it 'returns requested number of entries' do
234
+ 10.times { signal.compute(positive_sources) }
235
+ expect(signal.recent_rewards(5).size).to eq(5)
236
+ end
237
+ end
238
+
239
+ describe '#discounted_return' do
240
+ it 'returns 0.0 for empty history' do
241
+ expect(signal.discounted_return).to eq(0.0)
242
+ end
243
+
244
+ it 'computes discounted sum of rewards' do
245
+ 5.times { signal.compute(positive_sources) }
246
+ expect(signal.discounted_return).to be > 0.0
247
+ end
248
+
249
+ it 'recent rewards count more than older ones' do
250
+ full = signal.discounted_return(10)
251
+ expect(full).to eq(0.0)
252
+
253
+ 5.times { signal.compute(positive_sources) }
254
+ windowed = signal.discounted_return(3)
255
+ full_return = signal.discounted_return
256
+ expect(full_return).to be >= windowed
257
+ end
258
+ end
259
+
260
+ describe '#reward_volatility' do
261
+ it 'returns 0.0 with insufficient data' do
262
+ expect(signal.reward_volatility).to eq(0.0)
263
+ end
264
+
265
+ it 'is low for consistent rewards' do
266
+ 20.times { signal.compute(neutral_sources) }
267
+ expect(signal.reward_volatility).to be < 0.1
268
+ end
269
+
270
+ it 'is higher for alternating rewards' do
271
+ 10.times do
272
+ signal.compute(positive_sources)
273
+ signal.compute(negative_sources)
274
+ end
275
+ expect(signal.reward_volatility).to be > 0.0
276
+ end
277
+ end
278
+
279
+ describe '#to_h' do
280
+ it 'returns complete state hash' do
281
+ signal.compute(positive_sources)
282
+ h = signal.to_h
283
+ expect(h).to include(:running_average, :predicted_reward, :last_rpe, :rpe_class,
284
+ :tick_count, :learning_signal, :anhedonic, :euphoric,
285
+ :volatility, :domains_tracked, :history_size)
286
+ end
287
+ end
288
+
289
+ describe 'history cap' do
290
+ it 'caps at MAX_REWARD_HISTORY' do
291
+ max = Legion::Extensions::Agentic::Affect::Reward::Helpers::Constants::MAX_REWARD_HISTORY
292
+ (max + 10).times { signal.compute(positive_sources) }
293
+ expect(signal.history.size).to eq(max)
294
+ end
295
+ end
296
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Legion::Extensions::Agentic::Affect::Reward::Helpers::RewardStore do
6
+ subject(:store) { described_class.new }
7
+
8
+ let(:positive_tick) do
9
+ {
10
+ prediction_engine: { rolling_accuracy: 0.8, error_rate: 0.1 },
11
+ curiosity: { resolved_count: 2, intensity: 0.6 },
12
+ volition: { completed_count: 1, failed_count: 0, current_domain: :networking },
13
+ trust: { composite_delta: 0.1 },
14
+ flow: { in_flow: true, score: 0.8 },
15
+ attention: { novelty_score: 0.5, spotlight_count: 3 },
16
+ homeostasis: { worst_deviation: 0.1, allostatic_load: 0.1 }
17
+ }
18
+ end
19
+
20
+ let(:negative_tick) do
21
+ {
22
+ prediction_engine: { rolling_accuracy: 0.2, error_rate: 0.8 },
23
+ curiosity: { resolved_count: 0, intensity: 0.1 },
24
+ volition: { completed_count: 0, failed_count: 2, current_domain: :debugging },
25
+ trust: { composite_delta: -0.2 },
26
+ flow: { in_flow: false, score: 0.1 },
27
+ attention: { novelty_score: 0.0, spotlight_count: 0 },
28
+ homeostasis: { worst_deviation: 0.8, allostatic_load: 0.7 }
29
+ }
30
+ end
31
+
32
+ let(:empty_tick) { {} }
33
+
34
+ describe '#process_tick' do
35
+ it 'returns reward result hash' do
36
+ result = store.process_tick(positive_tick)
37
+ expect(result).to include(:reward, :rpe, :rpe_class, :running_average,
38
+ :predicted_reward, :sources, :learning_signal)
39
+ end
40
+
41
+ it 'computes positive reward for positive tick' do
42
+ result = store.process_tick(positive_tick)
43
+ expect(result[:reward]).to be > 0.0
44
+ end
45
+
46
+ it 'computes negative reward for negative tick' do
47
+ result = store.process_tick(negative_tick)
48
+ expect(result[:reward]).to be < 0.0
49
+ end
50
+
51
+ it 'handles empty tick results' do
52
+ result = store.process_tick(empty_tick)
53
+ expect(result[:reward]).to be_a(Float)
54
+ end
55
+
56
+ it 'records domain reward when domain available' do
57
+ store.process_tick(positive_tick)
58
+ expect(store.signal.domain_history[:networking]).not_to be_nil
59
+ end
60
+ end
61
+
62
+ describe '#domain_report' do
63
+ it 'returns report for known domain' do
64
+ store.process_tick(positive_tick)
65
+ report = store.domain_report(:networking)
66
+ expect(report).to include(:domain, :average, :trend, :history)
67
+ expect(report[:domain]).to eq(:networking)
68
+ end
69
+
70
+ it 'returns empty report for unknown domain' do
71
+ report = store.domain_report(:unknown)
72
+ expect(report[:average]).to eq(0.0)
73
+ expect(report[:trend]).to eq(:no_data)
74
+ end
75
+ end
76
+
77
+ describe '#all_domain_averages' do
78
+ it 'returns empty hash initially' do
79
+ expect(store.all_domain_averages).to be_empty
80
+ end
81
+
82
+ it 'tracks multiple domains' do
83
+ store.process_tick(positive_tick)
84
+ store.process_tick(negative_tick)
85
+ averages = store.all_domain_averages
86
+ expect(averages.keys).to include(:networking, :debugging)
87
+ end
88
+ end
89
+
90
+ describe '#health_assessment' do
91
+ it 'returns healthy initially' do
92
+ assessment = store.health_assessment
93
+ expect(assessment[:status]).to eq(:neutral).or eq(:healthy)
94
+ end
95
+
96
+ it 'detects anhedonia with persistent negative' do
97
+ 50.times { store.process_tick(negative_tick) }
98
+ assessment = store.health_assessment
99
+ expect(assessment[:status]).to eq(:anhedonic)
100
+ end
101
+
102
+ it 'detects euphoria with persistent positive' do
103
+ extreme = positive_tick.dup
104
+ extreme[:prediction_engine] = { rolling_accuracy: 1.0, error_rate: 0.0 }
105
+ extreme[:curiosity] = { resolved_count: 5, intensity: 1.0 }
106
+ extreme[:volition] = { completed_count: 3, failed_count: 0, current_domain: :test }
107
+ extreme[:flow] = { in_flow: true, score: 1.0 }
108
+ extreme[:attention] = { novelty_score: 1.0, spotlight_count: 5 }
109
+ extreme[:homeostasis] = { worst_deviation: 0.0, allostatic_load: 0.0 }
110
+ 50.times { store.process_tick(extreme) }
111
+ assessment = store.health_assessment
112
+ expect(assessment[:status]).to eq(:euphoric)
113
+ end
114
+
115
+ it 'includes severity' do
116
+ assessment = store.health_assessment
117
+ expect(assessment).to have_key(:severity)
118
+ end
119
+ end
120
+
121
+ describe 'signal extraction' do
122
+ it 'extracts prediction reward from accuracy' do
123
+ result = store.process_tick(positive_tick)
124
+ expect(result[:sources][:prediction_accuracy]).to be > 0.0
125
+ end
126
+
127
+ it 'extracts curiosity reward from resolved count' do
128
+ result = store.process_tick(positive_tick)
129
+ expect(result[:sources][:curiosity_resolved]).to be > 0.0
130
+ end
131
+
132
+ it 'extracts goal reward from completed count' do
133
+ result = store.process_tick(positive_tick)
134
+ expect(result[:sources][:goal_achieved]).to be > 0.0
135
+ end
136
+
137
+ it 'extracts social reward from trust delta' do
138
+ result = store.process_tick(positive_tick)
139
+ expect(result[:sources][:social_approval]).to be > 0.0
140
+ end
141
+
142
+ it 'extracts flow reward from flow state' do
143
+ result = store.process_tick(positive_tick)
144
+ expect(result[:sources][:flow_state]).to be > 0.0
145
+ end
146
+
147
+ it 'extracts error reward from error rate' do
148
+ result = store.process_tick(positive_tick)
149
+ expect(result[:sources][:error_avoidance]).to be > 0.0
150
+ end
151
+
152
+ it 'extracts novelty reward from attention' do
153
+ result = store.process_tick(positive_tick)
154
+ expect(result[:sources][:novelty_encounter]).to be > 0.0
155
+ end
156
+
157
+ it 'extracts homeostatic reward from deviation' do
158
+ result = store.process_tick(positive_tick)
159
+ expect(result[:sources][:homeostatic_balance]).to be > 0.0
160
+ end
161
+
162
+ it 'returns negative flow reward when not in flow' do
163
+ result = store.process_tick(negative_tick)
164
+ expect(result[:sources][:flow_state]).to be < 0.0
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Legion::Extensions::Agentic::Affect::Reward::Runners::Reward do
6
+ let(:client) { Legion::Extensions::Agentic::Affect::Reward::Client.new }
7
+
8
+ let(:positive_tick) do
9
+ {
10
+ prediction_engine: { rolling_accuracy: 0.8, error_rate: 0.1 },
11
+ curiosity: { resolved_count: 2, intensity: 0.6 },
12
+ volition: { completed_count: 1, failed_count: 0, current_domain: :test },
13
+ trust: { composite_delta: 0.1 },
14
+ flow: { in_flow: true, score: 0.7 },
15
+ attention: { novelty_score: 0.4, spotlight_count: 2 },
16
+ homeostasis: { worst_deviation: 0.1, allostatic_load: 0.1 }
17
+ }
18
+ end
19
+
20
+ describe '#compute_reward' do
21
+ it 'returns reward hash' do
22
+ result = client.compute_reward(tick_results: positive_tick)
23
+ expect(result).to include(:reward, :rpe, :rpe_class, :running_average,
24
+ :predicted_reward, :sources, :learning_signal)
25
+ end
26
+
27
+ it 'returns positive reward for positive tick' do
28
+ result = client.compute_reward(tick_results: positive_tick)
29
+ expect(result[:reward]).to be > 0.0
30
+ end
31
+
32
+ it 'returns reward in valid range' do
33
+ result = client.compute_reward(tick_results: positive_tick)
34
+ expect(result[:reward]).to be_between(-1.0, 1.0)
35
+ end
36
+
37
+ it 'handles empty tick results' do
38
+ result = client.compute_reward(tick_results: {})
39
+ expect(result[:reward]).to be_a(Float)
40
+ end
41
+ end
42
+
43
+ describe '#reward_status' do
44
+ it 'returns status with health assessment' do
45
+ client.compute_reward(tick_results: positive_tick)
46
+ status = client.reward_status
47
+ expect(status).to include(:running_average, :predicted_reward, :last_rpe,
48
+ :tick_count, :health)
49
+ expect(status[:health]).to include(:status, :severity)
50
+ end
51
+ end
52
+
53
+ describe '#reward_for' do
54
+ it 'returns domain report' do
55
+ client.compute_reward(tick_results: positive_tick)
56
+ report = client.reward_for(domain: :test)
57
+ expect(report).to include(:domain, :average, :trend, :history)
58
+ end
59
+
60
+ it 'returns empty for unknown domain' do
61
+ report = client.reward_for(domain: :unknown)
62
+ expect(report[:average]).to eq(0.0)
63
+ end
64
+ end
65
+
66
+ describe '#reward_history' do
67
+ it 'returns empty initially' do
68
+ result = client.reward_history
69
+ expect(result[:history]).to be_empty
70
+ expect(result[:total]).to eq(0)
71
+ end
72
+
73
+ it 'returns history after compute calls' do
74
+ 5.times { client.compute_reward(tick_results: positive_tick) }
75
+ result = client.reward_history
76
+ expect(result[:history].size).to eq(5)
77
+ expect(result[:total]).to eq(5)
78
+ end
79
+
80
+ it 'respects limit' do
81
+ 10.times { client.compute_reward(tick_results: positive_tick) }
82
+ result = client.reward_history(limit: 3)
83
+ expect(result[:history].size).to eq(3)
84
+ end
85
+
86
+ it 'includes discounted return' do
87
+ 5.times { client.compute_reward(tick_results: positive_tick) }
88
+ result = client.reward_history
89
+ expect(result[:discounted_return]).to be_a(Float)
90
+ end
91
+ end
92
+
93
+ describe '#domain_rewards' do
94
+ it 'returns empty initially' do
95
+ result = client.domain_rewards
96
+ expect(result[:domains]).to be_empty
97
+ expect(result[:domain_count]).to eq(0)
98
+ end
99
+
100
+ it 'tracks domains from tick results' do
101
+ client.compute_reward(tick_results: positive_tick)
102
+ result = client.domain_rewards
103
+ expect(result[:domain_count]).to be >= 1
104
+ end
105
+
106
+ it 'identifies best and worst domains' do
107
+ 5.times { client.compute_reward(tick_results: positive_tick) }
108
+ result = client.domain_rewards
109
+ expect(result[:best_domain]).not_to be_nil if result[:domain_count] > 0
110
+ end
111
+ end
112
+
113
+ describe '#reward_stats' do
114
+ it 'returns comprehensive stats' do
115
+ client.compute_reward(tick_results: positive_tick)
116
+ stats = client.reward_stats
117
+ expect(stats).to include(:running_average, :predicted_reward, :volatility,
118
+ :tick_count, :health, :domains_tracked,
119
+ :history_size, :discounted_return,
120
+ :anhedonic, :euphoric)
121
+ end
122
+ end
123
+
124
+ describe 'reward prediction error learning' do
125
+ it 'generates large RPE for unexpected positive after neutral' do
126
+ 10.times { client.compute_reward(tick_results: {}) }
127
+ result = client.compute_reward(tick_results: positive_tick)
128
+ expect(result[:rpe]).to be > 0.0
129
+ expect(result[:learning_signal]).to be true
130
+ end
131
+
132
+ it 'generates negative RPE for unexpected negative after positive' do
133
+ 10.times { client.compute_reward(tick_results: positive_tick) }
134
+ negative_tick = {
135
+ prediction_engine: { rolling_accuracy: 0.2, error_rate: 0.8 },
136
+ volition: { completed_count: 0, failed_count: 2 },
137
+ flow: { in_flow: false, score: 0.1 }
138
+ }
139
+ result = client.compute_reward(tick_results: negative_tick)
140
+ expect(result[:rpe]).to be < 0.0
141
+ end
142
+
143
+ it 'converges RPE to zero for stable rewards' do
144
+ 30.times { client.compute_reward(tick_results: positive_tick) }
145
+ result = client.compute_reward(tick_results: positive_tick)
146
+ expect(result[:rpe].abs).to be < 0.1
147
+ end
148
+ end
149
+ end