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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +12 -0
- data/Gemfile +5 -0
- data/LICENSE +21 -0
- data/README.md +13 -0
- data/lex-agentic-affect.gemspec +30 -0
- data/lib/legion/extensions/agentic/affect/appraisal/client.rb +20 -0
- data/lib/legion/extensions/agentic/affect/appraisal/helpers/appraisal.rb +112 -0
- data/lib/legion/extensions/agentic/affect/appraisal/helpers/appraisal_engine.rb +129 -0
- data/lib/legion/extensions/agentic/affect/appraisal/helpers/constants.rb +43 -0
- data/lib/legion/extensions/agentic/affect/appraisal/runners/appraisal.rb +105 -0
- data/lib/legion/extensions/agentic/affect/appraisal/version.rb +13 -0
- data/lib/legion/extensions/agentic/affect/appraisal.rb +19 -0
- data/lib/legion/extensions/agentic/affect/cognitive_empathy/client.rb +19 -0
- data/lib/legion/extensions/agentic/affect/cognitive_empathy/helpers/constants.rb +37 -0
- data/lib/legion/extensions/agentic/affect/cognitive_empathy/helpers/empathy_engine.rb +151 -0
- data/lib/legion/extensions/agentic/affect/cognitive_empathy/helpers/perspective.rb +92 -0
- data/lib/legion/extensions/agentic/affect/cognitive_empathy/runners/cognitive_empathy.rb +93 -0
- data/lib/legion/extensions/agentic/affect/cognitive_empathy/version.rb +13 -0
- data/lib/legion/extensions/agentic/affect/cognitive_empathy.rb +20 -0
- data/lib/legion/extensions/agentic/affect/contagion/client.rb +28 -0
- data/lib/legion/extensions/agentic/affect/contagion/helpers/constants.rb +34 -0
- data/lib/legion/extensions/agentic/affect/contagion/helpers/contagion_engine.rb +184 -0
- data/lib/legion/extensions/agentic/affect/contagion/helpers/meme.rb +97 -0
- data/lib/legion/extensions/agentic/affect/contagion/runners/cognitive_contagion.rb +125 -0
- data/lib/legion/extensions/agentic/affect/contagion/version.rb +13 -0
- data/lib/legion/extensions/agentic/affect/contagion.rb +19 -0
- data/lib/legion/extensions/agentic/affect/defusion/client.rb +28 -0
- data/lib/legion/extensions/agentic/affect/defusion/helpers/constants.rb +64 -0
- data/lib/legion/extensions/agentic/affect/defusion/helpers/defusion_engine.rb +167 -0
- data/lib/legion/extensions/agentic/affect/defusion/helpers/thought.rb +92 -0
- data/lib/legion/extensions/agentic/affect/defusion/runners/cognitive_defusion.rb +127 -0
- data/lib/legion/extensions/agentic/affect/defusion/version.rb +13 -0
- data/lib/legion/extensions/agentic/affect/defusion.rb +19 -0
- data/lib/legion/extensions/agentic/affect/emotion/actors/momentum_decay.rb +45 -0
- data/lib/legion/extensions/agentic/affect/emotion/client.rb +36 -0
- data/lib/legion/extensions/agentic/affect/emotion/helpers/baseline.rb +52 -0
- data/lib/legion/extensions/agentic/affect/emotion/helpers/momentum.rb +52 -0
- data/lib/legion/extensions/agentic/affect/emotion/helpers/valence.rb +92 -0
- data/lib/legion/extensions/agentic/affect/emotion/runners/gut.rb +102 -0
- data/lib/legion/extensions/agentic/affect/emotion/runners/valence.rb +120 -0
- data/lib/legion/extensions/agentic/affect/emotion/version.rb +13 -0
- data/lib/legion/extensions/agentic/affect/emotion.rb +20 -0
- data/lib/legion/extensions/agentic/affect/empathy/client.rb +21 -0
- data/lib/legion/extensions/agentic/affect/empathy/helpers/constants.rb +54 -0
- data/lib/legion/extensions/agentic/affect/empathy/helpers/mental_model.rb +185 -0
- data/lib/legion/extensions/agentic/affect/empathy/helpers/model_store.rb +88 -0
- data/lib/legion/extensions/agentic/affect/empathy/runners/empathy.rb +173 -0
- data/lib/legion/extensions/agentic/affect/empathy/version.rb +13 -0
- data/lib/legion/extensions/agentic/affect/empathy.rb +20 -0
- data/lib/legion/extensions/agentic/affect/fatigue/client.rb +26 -0
- data/lib/legion/extensions/agentic/affect/fatigue/helpers/constants.rb +54 -0
- data/lib/legion/extensions/agentic/affect/fatigue/helpers/energy_model.rb +181 -0
- data/lib/legion/extensions/agentic/affect/fatigue/helpers/fatigue_store.rb +146 -0
- data/lib/legion/extensions/agentic/affect/fatigue/runners/fatigue.rb +89 -0
- data/lib/legion/extensions/agentic/affect/fatigue/version.rb +13 -0
- data/lib/legion/extensions/agentic/affect/fatigue.rb +19 -0
- data/lib/legion/extensions/agentic/affect/flow/client.rb +25 -0
- data/lib/legion/extensions/agentic/affect/flow/helpers/constants.rb +84 -0
- data/lib/legion/extensions/agentic/affect/flow/helpers/flow_detector.rb +166 -0
- data/lib/legion/extensions/agentic/affect/flow/runners/flow.rb +129 -0
- data/lib/legion/extensions/agentic/affect/flow/version.rb +13 -0
- data/lib/legion/extensions/agentic/affect/flow.rb +18 -0
- data/lib/legion/extensions/agentic/affect/interoception/actors/decay.rb +45 -0
- data/lib/legion/extensions/agentic/affect/interoception/client.rb +28 -0
- data/lib/legion/extensions/agentic/affect/interoception/helpers/body_budget.rb +152 -0
- data/lib/legion/extensions/agentic/affect/interoception/helpers/constants.rb +68 -0
- data/lib/legion/extensions/agentic/affect/interoception/helpers/somatic_marker.rb +75 -0
- data/lib/legion/extensions/agentic/affect/interoception/runners/interoception.rb +101 -0
- data/lib/legion/extensions/agentic/affect/interoception/version.rb +13 -0
- data/lib/legion/extensions/agentic/affect/interoception.rb +20 -0
- data/lib/legion/extensions/agentic/affect/mood/client.rb +21 -0
- data/lib/legion/extensions/agentic/affect/mood/helpers/constants.rb +78 -0
- data/lib/legion/extensions/agentic/affect/mood/helpers/mood_state.rb +154 -0
- data/lib/legion/extensions/agentic/affect/mood/runners/mood.rb +122 -0
- data/lib/legion/extensions/agentic/affect/mood/version.rb +13 -0
- data/lib/legion/extensions/agentic/affect/mood.rb +18 -0
- data/lib/legion/extensions/agentic/affect/motivation/client.rb +26 -0
- data/lib/legion/extensions/agentic/affect/motivation/helpers/constants.rb +48 -0
- data/lib/legion/extensions/agentic/affect/motivation/helpers/drive_state.rb +98 -0
- data/lib/legion/extensions/agentic/affect/motivation/helpers/motivation_store.rb +106 -0
- data/lib/legion/extensions/agentic/affect/motivation/runners/motivation.rb +165 -0
- data/lib/legion/extensions/agentic/affect/motivation/version.rb +13 -0
- data/lib/legion/extensions/agentic/affect/motivation.rb +19 -0
- data/lib/legion/extensions/agentic/affect/reappraisal/actors/auto_regulate.rb +45 -0
- data/lib/legion/extensions/agentic/affect/reappraisal/client.rb +28 -0
- data/lib/legion/extensions/agentic/affect/reappraisal/helpers/constants.rb +82 -0
- data/lib/legion/extensions/agentic/affect/reappraisal/helpers/emotional_event.rb +98 -0
- data/lib/legion/extensions/agentic/affect/reappraisal/helpers/llm_enhancer.rb +88 -0
- data/lib/legion/extensions/agentic/affect/reappraisal/helpers/reappraisal_engine.rb +153 -0
- data/lib/legion/extensions/agentic/affect/reappraisal/runners/cognitive_reappraisal.rb +164 -0
- data/lib/legion/extensions/agentic/affect/reappraisal/version.rb +13 -0
- data/lib/legion/extensions/agentic/affect/reappraisal.rb +20 -0
- data/lib/legion/extensions/agentic/affect/regulation/client.rb +25 -0
- data/lib/legion/extensions/agentic/affect/regulation/helpers/constants.rb +71 -0
- data/lib/legion/extensions/agentic/affect/regulation/helpers/regulation_model.rb +175 -0
- data/lib/legion/extensions/agentic/affect/regulation/runners/emotional_regulation.rb +127 -0
- data/lib/legion/extensions/agentic/affect/regulation/version.rb +13 -0
- data/lib/legion/extensions/agentic/affect/regulation.rb +18 -0
- data/lib/legion/extensions/agentic/affect/resilience/client.rb +27 -0
- data/lib/legion/extensions/agentic/affect/resilience/helpers/adversity_tracker.rb +130 -0
- data/lib/legion/extensions/agentic/affect/resilience/helpers/constants.rb +79 -0
- data/lib/legion/extensions/agentic/affect/resilience/helpers/resilience_model.rb +165 -0
- data/lib/legion/extensions/agentic/affect/resilience/runners/resilience.rb +150 -0
- data/lib/legion/extensions/agentic/affect/resilience/version.rb +13 -0
- data/lib/legion/extensions/agentic/affect/resilience.rb +19 -0
- data/lib/legion/extensions/agentic/affect/resonance/client.rb +24 -0
- data/lib/legion/extensions/agentic/affect/resonance/helpers/category.rb +75 -0
- data/lib/legion/extensions/agentic/affect/resonance/helpers/constants.rb +47 -0
- data/lib/legion/extensions/agentic/affect/resonance/helpers/resonance_engine.rb +115 -0
- data/lib/legion/extensions/agentic/affect/resonance/runners/cognitive_resonance.rb +94 -0
- data/lib/legion/extensions/agentic/affect/resonance/version.rb +13 -0
- data/lib/legion/extensions/agentic/affect/resonance.rb +19 -0
- data/lib/legion/extensions/agentic/affect/reward/client.rb +26 -0
- data/lib/legion/extensions/agentic/affect/reward/helpers/constants.rb +67 -0
- data/lib/legion/extensions/agentic/affect/reward/helpers/reward_signal.rb +178 -0
- data/lib/legion/extensions/agentic/affect/reward/helpers/reward_store.rb +142 -0
- data/lib/legion/extensions/agentic/affect/reward/runners/reward.rb +92 -0
- data/lib/legion/extensions/agentic/affect/reward/version.rb +13 -0
- data/lib/legion/extensions/agentic/affect/reward.rb +19 -0
- data/lib/legion/extensions/agentic/affect/somatic_marker/actors/decay.rb +45 -0
- data/lib/legion/extensions/agentic/affect/somatic_marker/client.rb +29 -0
- data/lib/legion/extensions/agentic/affect/somatic_marker/helpers/body_state.rb +69 -0
- data/lib/legion/extensions/agentic/affect/somatic_marker/helpers/constants.rb +43 -0
- data/lib/legion/extensions/agentic/affect/somatic_marker/helpers/marker_store.rb +160 -0
- data/lib/legion/extensions/agentic/affect/somatic_marker/helpers/somatic_marker.rb +74 -0
- data/lib/legion/extensions/agentic/affect/somatic_marker/runners/somatic_marker.rb +132 -0
- data/lib/legion/extensions/agentic/affect/somatic_marker/version.rb +13 -0
- data/lib/legion/extensions/agentic/affect/somatic_marker.rb +20 -0
- data/lib/legion/extensions/agentic/affect/version.rb +11 -0
- data/lib/legion/extensions/agentic/affect.rb +34 -0
- data/spec/legion/extensions/agentic/affect/appraisal/client_spec.rb +52 -0
- data/spec/legion/extensions/agentic/affect/appraisal/helpers/appraisal_engine_spec.rb +161 -0
- data/spec/legion/extensions/agentic/affect/appraisal/helpers/appraisal_spec.rb +175 -0
- data/spec/legion/extensions/agentic/affect/appraisal/helpers/constants_spec.rb +49 -0
- data/spec/legion/extensions/agentic/affect/appraisal/runners/appraisal_spec.rb +116 -0
- data/spec/legion/extensions/agentic/affect/cognitive_empathy/client_spec.rb +62 -0
- data/spec/legion/extensions/agentic/affect/cognitive_empathy/helpers/empathy_engine_spec.rb +316 -0
- data/spec/legion/extensions/agentic/affect/cognitive_empathy/helpers/perspective_spec.rb +132 -0
- data/spec/legion/extensions/agentic/affect/cognitive_empathy/runners/cognitive_empathy_spec.rb +200 -0
- data/spec/legion/extensions/agentic/affect/contagion/client_spec.rb +63 -0
- data/spec/legion/extensions/agentic/affect/contagion/helpers/constants_spec.rb +86 -0
- data/spec/legion/extensions/agentic/affect/contagion/helpers/contagion_engine_spec.rb +241 -0
- data/spec/legion/extensions/agentic/affect/contagion/helpers/meme_spec.rb +160 -0
- data/spec/legion/extensions/agentic/affect/contagion/runners/cognitive_contagion_spec.rb +211 -0
- data/spec/legion/extensions/agentic/affect/defusion/client_spec.rb +80 -0
- data/spec/legion/extensions/agentic/affect/defusion/helpers/constants_spec.rb +84 -0
- data/spec/legion/extensions/agentic/affect/defusion/helpers/defusion_engine_spec.rb +250 -0
- data/spec/legion/extensions/agentic/affect/defusion/helpers/thought_spec.rb +178 -0
- data/spec/legion/extensions/agentic/affect/defusion/runners/cognitive_defusion_spec.rb +185 -0
- data/spec/legion/extensions/agentic/affect/emotion/actors/momentum_decay_spec.rb +46 -0
- data/spec/legion/extensions/agentic/affect/emotion/client_spec.rb +46 -0
- data/spec/legion/extensions/agentic/affect/emotion/helpers/baseline_spec.rb +48 -0
- data/spec/legion/extensions/agentic/affect/emotion/helpers/momentum_spec.rb +45 -0
- data/spec/legion/extensions/agentic/affect/emotion/helpers/valence_spec.rb +91 -0
- data/spec/legion/extensions/agentic/affect/emotion/runners/gut_spec.rb +73 -0
- data/spec/legion/extensions/agentic/affect/emotion/runners/valence_spec.rb +67 -0
- data/spec/legion/extensions/agentic/affect/empathy/client_spec.rb +20 -0
- data/spec/legion/extensions/agentic/affect/empathy/helpers/constants_spec.rb +23 -0
- data/spec/legion/extensions/agentic/affect/empathy/helpers/mental_model_spec.rb +150 -0
- data/spec/legion/extensions/agentic/affect/empathy/helpers/model_store_spec.rb +94 -0
- data/spec/legion/extensions/agentic/affect/empathy/runners/empathy_spec.rb +127 -0
- data/spec/legion/extensions/agentic/affect/fatigue/client_spec.rb +66 -0
- data/spec/legion/extensions/agentic/affect/fatigue/helpers/constants_spec.rb +130 -0
- data/spec/legion/extensions/agentic/affect/fatigue/helpers/energy_model_spec.rb +281 -0
- data/spec/legion/extensions/agentic/affect/fatigue/helpers/fatigue_store_spec.rb +157 -0
- data/spec/legion/extensions/agentic/affect/fatigue/runners/fatigue_spec.rb +127 -0
- data/spec/legion/extensions/agentic/affect/flow/client_spec.rb +58 -0
- data/spec/legion/extensions/agentic/affect/flow/helpers/constants_spec.rb +112 -0
- data/spec/legion/extensions/agentic/affect/flow/helpers/flow_detector_spec.rb +268 -0
- data/spec/legion/extensions/agentic/affect/flow/runners/flow_spec.rb +222 -0
- data/spec/legion/extensions/agentic/affect/interoception/client_spec.rb +52 -0
- data/spec/legion/extensions/agentic/affect/interoception/helpers/body_budget_spec.rb +178 -0
- data/spec/legion/extensions/agentic/affect/interoception/helpers/somatic_marker_spec.rb +120 -0
- data/spec/legion/extensions/agentic/affect/interoception/runners/interoception_spec.rb +108 -0
- data/spec/legion/extensions/agentic/affect/mood/client_spec.rb +20 -0
- data/spec/legion/extensions/agentic/affect/mood/helpers/constants_spec.rb +29 -0
- data/spec/legion/extensions/agentic/affect/mood/helpers/mood_state_spec.rb +94 -0
- data/spec/legion/extensions/agentic/affect/mood/runners/mood_spec.rb +71 -0
- data/spec/legion/extensions/agentic/affect/motivation/client_spec.rb +35 -0
- data/spec/legion/extensions/agentic/affect/motivation/helpers/constants_spec.rb +111 -0
- data/spec/legion/extensions/agentic/affect/motivation/helpers/drive_state_spec.rb +183 -0
- data/spec/legion/extensions/agentic/affect/motivation/helpers/motivation_store_spec.rb +185 -0
- data/spec/legion/extensions/agentic/affect/motivation/runners/motivation_spec.rb +248 -0
- data/spec/legion/extensions/agentic/affect/reappraisal/actors/auto_regulate_spec.rb +46 -0
- data/spec/legion/extensions/agentic/affect/reappraisal/client_spec.rb +64 -0
- data/spec/legion/extensions/agentic/affect/reappraisal/helpers/constants_spec.rb +102 -0
- data/spec/legion/extensions/agentic/affect/reappraisal/helpers/emotional_event_spec.rb +177 -0
- data/spec/legion/extensions/agentic/affect/reappraisal/helpers/llm_enhancer_spec.rb +161 -0
- data/spec/legion/extensions/agentic/affect/reappraisal/helpers/reappraisal_engine_spec.rb +211 -0
- data/spec/legion/extensions/agentic/affect/reappraisal/runners/cognitive_reappraisal_spec.rb +312 -0
- data/spec/legion/extensions/agentic/affect/regulation/client_spec.rb +61 -0
- data/spec/legion/extensions/agentic/affect/regulation/helpers/constants_spec.rb +108 -0
- data/spec/legion/extensions/agentic/affect/regulation/helpers/regulation_model_spec.rb +200 -0
- data/spec/legion/extensions/agentic/affect/regulation/runners/emotional_regulation_spec.rb +190 -0
- data/spec/legion/extensions/agentic/affect/resilience/client_spec.rb +36 -0
- data/spec/legion/extensions/agentic/affect/resilience/helpers/adversity_tracker_spec.rb +164 -0
- data/spec/legion/extensions/agentic/affect/resilience/helpers/constants_spec.rb +78 -0
- data/spec/legion/extensions/agentic/affect/resilience/helpers/resilience_model_spec.rb +133 -0
- data/spec/legion/extensions/agentic/affect/resilience/runners/resilience_spec.rb +150 -0
- data/spec/legion/extensions/agentic/affect/resonance/client_spec.rb +66 -0
- data/spec/legion/extensions/agentic/affect/resonance/cognitive_resonance_spec.rb +27 -0
- data/spec/legion/extensions/agentic/affect/resonance/helpers/category_spec.rb +146 -0
- data/spec/legion/extensions/agentic/affect/resonance/helpers/constants_spec.rb +104 -0
- data/spec/legion/extensions/agentic/affect/resonance/helpers/resonance_engine_spec.rb +189 -0
- data/spec/legion/extensions/agentic/affect/resonance/runners/cognitive_resonance_spec.rb +197 -0
- data/spec/legion/extensions/agentic/affect/reward/client_spec.rb +42 -0
- data/spec/legion/extensions/agentic/affect/reward/helpers/constants_spec.rb +91 -0
- data/spec/legion/extensions/agentic/affect/reward/helpers/reward_signal_spec.rb +296 -0
- data/spec/legion/extensions/agentic/affect/reward/helpers/reward_store_spec.rb +167 -0
- data/spec/legion/extensions/agentic/affect/reward/runners/reward_spec.rb +149 -0
- data/spec/legion/extensions/agentic/affect/somatic_marker/client_spec.rb +83 -0
- data/spec/legion/extensions/agentic/affect/somatic_marker/helpers/body_state_spec.rb +155 -0
- data/spec/legion/extensions/agentic/affect/somatic_marker/helpers/marker_store_spec.rb +233 -0
- data/spec/legion/extensions/agentic/affect/somatic_marker/helpers/somatic_marker_spec.rb +172 -0
- data/spec/legion/extensions/agentic/affect/somatic_marker/runners/somatic_marker_spec.rb +181 -0
- data/spec/spec_helper.rb +46 -0
- metadata +302 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/agentic/affect/somatic_marker/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Agentic::Affect::SomaticMarker::Client do
|
|
6
|
+
let(:client) { described_class.new }
|
|
7
|
+
|
|
8
|
+
it 'responds to all runner methods' do
|
|
9
|
+
expect(client).to respond_to(:register_marker)
|
|
10
|
+
expect(client).to respond_to(:evaluate_option)
|
|
11
|
+
expect(client).to respond_to(:make_decision)
|
|
12
|
+
expect(client).to respond_to(:reinforce)
|
|
13
|
+
expect(client).to respond_to(:update_body)
|
|
14
|
+
expect(client).to respond_to(:body_state)
|
|
15
|
+
expect(client).to respond_to(:markers_for_action)
|
|
16
|
+
expect(client).to respond_to(:recent_decisions)
|
|
17
|
+
expect(client).to respond_to(:update_somatic_markers)
|
|
18
|
+
expect(client).to respond_to(:somatic_marker_stats)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'maintains isolated state per instance' do
|
|
22
|
+
client_a = described_class.new
|
|
23
|
+
client_b = described_class.new
|
|
24
|
+
|
|
25
|
+
client_a.register_marker(action: :deploy, domain: :ops, valence: 0.9)
|
|
26
|
+
result_b = client_b.evaluate_option(action: :deploy, domain: :ops)
|
|
27
|
+
|
|
28
|
+
expect(client_a.somatic_marker_stats[:marker_count]).to eq(1)
|
|
29
|
+
expect(client_b.somatic_marker_stats[:marker_count]).to eq(0)
|
|
30
|
+
expect(result_b[:signal]).to eq(:neutral)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'runs a full decision cycle' do
|
|
34
|
+
# Register markers from past experience
|
|
35
|
+
client.register_marker(action: :merge, domain: :git, valence: 0.7, source: :experience)
|
|
36
|
+
client.register_marker(action: :revert, domain: :git, valence: -0.6, source: :experience)
|
|
37
|
+
client.register_marker(action: :hotfix, domain: :git, valence: 0.3, source: :inference)
|
|
38
|
+
|
|
39
|
+
# Make a decision
|
|
40
|
+
result = client.make_decision(options: %i[merge revert hotfix], domain: :git)
|
|
41
|
+
expect(result[:success]).to be true
|
|
42
|
+
expect(result[:decision][:ranked].first[:action]).to eq(:merge)
|
|
43
|
+
|
|
44
|
+
# Reinforce with negative outcome (merge caused issues)
|
|
45
|
+
marker_id = client.markers_for_action(action: :merge, domain: :git)[:markers].first[:id]
|
|
46
|
+
client.reinforce(marker_id: marker_id, outcome_valence: -0.8)
|
|
47
|
+
|
|
48
|
+
# After reinforcement, evaluate again
|
|
49
|
+
eval_result = client.evaluate_option(action: :merge, domain: :git)
|
|
50
|
+
# Valence should have moved toward negative
|
|
51
|
+
expect(eval_result[:valence]).to be < 0.7
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'body state affects decision context' do
|
|
55
|
+
client.register_marker(action: :take_risk, domain: :strategy, valence: 0.1)
|
|
56
|
+
|
|
57
|
+
# Under stress, body state should be surfaced
|
|
58
|
+
client.update_body(tension: 0.9, comfort: 0.1)
|
|
59
|
+
result = client.make_decision(options: %i[take_risk play_safe], domain: :strategy)
|
|
60
|
+
expect(result[:decision][:body_contribution][:stressed]).to be true
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'decay removes faded markers over time' do
|
|
64
|
+
client.register_marker(action: :old_action, domain: :history, valence: 0.0)
|
|
65
|
+
|
|
66
|
+
# Run enough decay cycles to fade the marker
|
|
67
|
+
60.times { client.update_somatic_markers }
|
|
68
|
+
|
|
69
|
+
stats = client.somatic_marker_stats
|
|
70
|
+
expect(stats[:marker_count]).to eq(0)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'tracks multiple domains independently' do
|
|
74
|
+
client.register_marker(action: :approve, domain: :finance, valence: 0.8)
|
|
75
|
+
client.register_marker(action: :approve, domain: :security, valence: -0.7)
|
|
76
|
+
|
|
77
|
+
finance_result = client.evaluate_option(action: :approve, domain: :finance)
|
|
78
|
+
security_result = client.evaluate_option(action: :approve, domain: :security)
|
|
79
|
+
|
|
80
|
+
expect(finance_result[:signal]).to eq(:approach)
|
|
81
|
+
expect(security_result[:signal]).to eq(:avoid)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::Agentic::Affect::SomaticMarker::Helpers::BodyState do
|
|
4
|
+
subject(:state) { described_class.new }
|
|
5
|
+
|
|
6
|
+
describe '#initialize' do
|
|
7
|
+
it 'sets neutral defaults' do
|
|
8
|
+
expect(state.arousal).to eq(0.5)
|
|
9
|
+
expect(state.tension).to eq(0.5)
|
|
10
|
+
expect(state.comfort).to eq(0.5)
|
|
11
|
+
expect(state.gut_signal).to eq(0.0)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'accepts custom values' do
|
|
15
|
+
s = described_class.new(arousal: 0.8, tension: 0.2, comfort: 0.9, gut_signal: 0.4)
|
|
16
|
+
expect(s.arousal).to eq(0.8)
|
|
17
|
+
expect(s.tension).to eq(0.2)
|
|
18
|
+
expect(s.comfort).to eq(0.9)
|
|
19
|
+
expect(s.gut_signal).to eq(0.4)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'clamps values to valid ranges' do
|
|
23
|
+
s = described_class.new(arousal: 2.0, tension: -1.0, comfort: 1.5, gut_signal: -2.0)
|
|
24
|
+
expect(s.arousal).to eq(1.0)
|
|
25
|
+
expect(s.tension).to eq(0.0)
|
|
26
|
+
expect(s.comfort).to eq(1.0)
|
|
27
|
+
expect(s.gut_signal).to eq(-1.0)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe '#composite_valence' do
|
|
32
|
+
it 'returns a value in reasonable range for neutral state' do
|
|
33
|
+
# neutral: comfort=0.5, tension=0.5, gut=0.0
|
|
34
|
+
# => (0.5*0.4) + ((1-0.5)*0.3) + (0.0*0.3) = 0.2 + 0.15 + 0.0 = 0.35
|
|
35
|
+
val = state.composite_valence
|
|
36
|
+
expect(val).to be_within(0.001).of(0.35)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'returns higher value for comfortable low-tension state' do
|
|
40
|
+
high = described_class.new(comfort: 1.0, tension: 0.0, gut_signal: 1.0)
|
|
41
|
+
low = described_class.new(comfort: 0.0, tension: 1.0, gut_signal: -1.0)
|
|
42
|
+
expect(high.composite_valence).to be > low.composite_valence
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'weights comfort at 0.4' do
|
|
46
|
+
s = described_class.new(comfort: 1.0, tension: 0.5, gut_signal: 0.0)
|
|
47
|
+
neutral = described_class.new(comfort: 0.0, tension: 0.5, gut_signal: 0.0)
|
|
48
|
+
diff = s.composite_valence - neutral.composite_valence
|
|
49
|
+
expect(diff).to be_within(0.001).of(0.4)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'weights tension at 0.3 (inverted)' do
|
|
53
|
+
low_tension = described_class.new(comfort: 0.5, tension: 0.0, gut_signal: 0.0)
|
|
54
|
+
high_tension = described_class.new(comfort: 0.5, tension: 1.0, gut_signal: 0.0)
|
|
55
|
+
diff = low_tension.composite_valence - high_tension.composite_valence
|
|
56
|
+
expect(diff).to be_within(0.001).of(0.3)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'weights gut_signal at 0.3' do
|
|
60
|
+
pos = described_class.new(comfort: 0.5, tension: 0.5, gut_signal: 1.0)
|
|
61
|
+
neg = described_class.new(comfort: 0.5, tension: 0.5, gut_signal: -1.0)
|
|
62
|
+
diff = pos.composite_valence - neg.composite_valence
|
|
63
|
+
expect(diff).to be_within(0.001).of(0.6)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
describe '#update' do
|
|
68
|
+
it 'updates individual fields' do
|
|
69
|
+
state.update(arousal: 0.9)
|
|
70
|
+
expect(state.arousal).to eq(0.9)
|
|
71
|
+
expect(state.tension).to eq(0.5)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'clamps updated values' do
|
|
75
|
+
state.update(tension: 1.5)
|
|
76
|
+
expect(state.tension).to eq(1.0)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'ignores nil fields' do
|
|
80
|
+
state.update(comfort: nil)
|
|
81
|
+
expect(state.comfort).to eq(0.5)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
describe '#decay' do
|
|
86
|
+
it 'drifts arousal toward 0.5' do
|
|
87
|
+
high = described_class.new(arousal: 0.9)
|
|
88
|
+
high.decay
|
|
89
|
+
expect(high.arousal).to be < 0.9
|
|
90
|
+
expect(high.arousal).to be >= 0.5
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'drifts tension toward 0.5' do
|
|
94
|
+
low = described_class.new(tension: 0.1)
|
|
95
|
+
low.decay
|
|
96
|
+
expect(low.tension).to be > 0.1
|
|
97
|
+
expect(low.tension).to be <= 0.5
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'drifts gut_signal toward 0.0' do
|
|
101
|
+
pos = described_class.new(gut_signal: 0.8)
|
|
102
|
+
pos.decay
|
|
103
|
+
expect(pos.gut_signal).to be < 0.8
|
|
104
|
+
expect(pos.gut_signal).to be >= 0.0
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'decays negative gut_signal toward 0.0' do
|
|
108
|
+
neg = described_class.new(gut_signal: -0.8)
|
|
109
|
+
neg.decay
|
|
110
|
+
expect(neg.gut_signal).to be > -0.8
|
|
111
|
+
expect(neg.gut_signal).to be <= 0.0
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'does not overshoot neutral targets' do
|
|
115
|
+
high_arousal = described_class.new(arousal: 0.53)
|
|
116
|
+
high_arousal.decay
|
|
117
|
+
expect(high_arousal.arousal).to be >= 0.5
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
describe '#stressed?' do
|
|
122
|
+
it 'returns true when tension > 0.7 and comfort < 0.3' do
|
|
123
|
+
stressed = described_class.new(tension: 0.8, comfort: 0.2)
|
|
124
|
+
expect(stressed.stressed?).to be true
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it 'returns false when tension is moderate' do
|
|
128
|
+
relaxed = described_class.new(tension: 0.5, comfort: 0.2)
|
|
129
|
+
expect(relaxed.stressed?).to be false
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it 'returns false when comfort is moderate' do
|
|
133
|
+
not_stressed = described_class.new(tension: 0.8, comfort: 0.5)
|
|
134
|
+
expect(not_stressed.stressed?).to be false
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it 'returns false for default state' do
|
|
138
|
+
expect(state.stressed?).to be false
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
describe '#to_h' do
|
|
143
|
+
it 'includes all state keys' do
|
|
144
|
+
h = state.to_h
|
|
145
|
+
expect(h).to include(:arousal, :tension, :comfort, :gut_signal, :composite_valence, :stressed)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it 'reflects current values' do
|
|
149
|
+
s = described_class.new(arousal: 0.7, tension: 0.8, comfort: 0.2, gut_signal: -0.5)
|
|
150
|
+
h = s.to_h
|
|
151
|
+
expect(h[:arousal]).to eq(0.7)
|
|
152
|
+
expect(h[:stressed]).to be true
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::Agentic::Affect::SomaticMarker::Helpers::MarkerStore do
|
|
4
|
+
subject(:store) { described_class.new }
|
|
5
|
+
|
|
6
|
+
describe '#register_marker' do
|
|
7
|
+
it 'creates a new marker' do
|
|
8
|
+
marker = store.register_marker(action: :deploy, domain: :ops, valence: 0.7)
|
|
9
|
+
expect(marker).to be_a(Legion::Extensions::Agentic::Affect::SomaticMarker::Helpers::SomaticMarker)
|
|
10
|
+
expect(marker.action).to eq(:deploy)
|
|
11
|
+
expect(marker.domain).to eq(:ops)
|
|
12
|
+
expect(marker.valence).to be_within(0.001).of(0.7)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'stores the marker in the store' do
|
|
16
|
+
store.register_marker(action: :deploy, domain: :ops, valence: 0.5)
|
|
17
|
+
expect(store.markers.size).to eq(1)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'assigns sequential ids' do
|
|
21
|
+
m1 = store.register_marker(action: :first, domain: :d, valence: 0.1)
|
|
22
|
+
m2 = store.register_marker(action: :second, domain: :d, valence: 0.2)
|
|
23
|
+
expect(m1.id).to eq('sm_1')
|
|
24
|
+
expect(m2.id).to eq('sm_2')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'accepts custom source' do
|
|
28
|
+
marker = store.register_marker(action: :deploy, domain: :ops, valence: 0.5, source: :instruction)
|
|
29
|
+
expect(marker.source).to eq(:instruction)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'evicts weakest marker when at MAX_MARKERS capacity' do
|
|
33
|
+
max = Legion::Extensions::Agentic::Affect::SomaticMarker::Helpers::Constants::MAX_MARKERS
|
|
34
|
+
|
|
35
|
+
# Fill store with mid-strength markers
|
|
36
|
+
max.times do |i|
|
|
37
|
+
store.register_marker(action: :"action_#{i}", domain: :d, valence: 0.0)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Manually weaken one
|
|
41
|
+
weakest = store.markers.values.first
|
|
42
|
+
50.times { weakest.decay }
|
|
43
|
+
|
|
44
|
+
initial_count = store.markers.size
|
|
45
|
+
store.register_marker(action: :overflow, domain: :d, valence: 0.5)
|
|
46
|
+
expect(store.markers.size).to eq(initial_count)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe '#evaluate_option' do
|
|
51
|
+
it 'returns neutral signal with no markers' do
|
|
52
|
+
result = store.evaluate_option(action: :deploy, domain: :ops)
|
|
53
|
+
expect(result[:signal]).to eq(:neutral)
|
|
54
|
+
expect(result[:marker_count]).to eq(0)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'returns approach signal for positive markers' do
|
|
58
|
+
store.register_marker(action: :deploy, domain: :ops, valence: 0.9)
|
|
59
|
+
result = store.evaluate_option(action: :deploy, domain: :ops)
|
|
60
|
+
expect(result[:signal]).to eq(:approach)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'returns avoid signal for negative markers' do
|
|
64
|
+
store.register_marker(action: :deploy, domain: :ops, valence: -0.9)
|
|
65
|
+
result = store.evaluate_option(action: :deploy, domain: :ops)
|
|
66
|
+
expect(result[:signal]).to eq(:avoid)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'weighs markers by strength' do
|
|
70
|
+
store.register_marker(action: :deploy, domain: :ops, valence: 0.9)
|
|
71
|
+
# Weaken the positive marker heavily
|
|
72
|
+
marker = store.markers.values.first
|
|
73
|
+
40.times { marker.decay }
|
|
74
|
+
|
|
75
|
+
store.register_marker(action: :deploy, domain: :ops, valence: -0.9)
|
|
76
|
+
|
|
77
|
+
result = store.evaluate_option(action: :deploy, domain: :ops)
|
|
78
|
+
# Strong negative should outweigh weak positive
|
|
79
|
+
expect(result[:valence]).to be < 0
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'only considers markers matching action and domain' do
|
|
83
|
+
store.register_marker(action: :deploy, domain: :ops, valence: 0.9)
|
|
84
|
+
store.register_marker(action: :rollback, domain: :ops, valence: -0.9)
|
|
85
|
+
store.register_marker(action: :deploy, domain: :dev, valence: -0.9)
|
|
86
|
+
|
|
87
|
+
result = store.evaluate_option(action: :deploy, domain: :ops)
|
|
88
|
+
expect(result[:signal]).to eq(:approach)
|
|
89
|
+
expect(result[:marker_count]).to eq(1)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
describe '#decide' do
|
|
94
|
+
it 'returns ranked options' do
|
|
95
|
+
store.register_marker(action: :approve, domain: :risk, valence: 0.8)
|
|
96
|
+
store.register_marker(action: :reject, domain: :risk, valence: -0.8)
|
|
97
|
+
|
|
98
|
+
result = store.decide(options: %i[approve reject], domain: :risk)
|
|
99
|
+
expect(result[:ranked].first[:action]).to eq(:approve)
|
|
100
|
+
expect(result[:ranked].last[:action]).to eq(:reject)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it 'records decision in history' do
|
|
104
|
+
store.decide(options: %i[go stop], domain: :ops)
|
|
105
|
+
expect(store.decision_history.size).to eq(1)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'caps options at MAX_OPTIONS_PER_DECISION' do
|
|
109
|
+
max = Legion::Extensions::Agentic::Affect::SomaticMarker::Helpers::Constants::MAX_OPTIONS_PER_DECISION
|
|
110
|
+
options = (max + 5).times.map { |i| :"option_#{i}" }
|
|
111
|
+
result = store.decide(options: options, domain: :ops)
|
|
112
|
+
expect(result[:ranked].size).to eq(max)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'includes body_contribution in result' do
|
|
116
|
+
result = store.decide(options: %i[act wait], domain: :ops)
|
|
117
|
+
expect(result).to have_key(:body_contribution)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it 'caps decision history at MAX_DECISION_HISTORY' do
|
|
121
|
+
max = Legion::Extensions::Agentic::Affect::SomaticMarker::Helpers::Constants::MAX_DECISION_HISTORY
|
|
122
|
+
(max + 5).times { store.decide(options: %i[go stop], domain: :ops) }
|
|
123
|
+
expect(store.decision_history(limit: max + 10).size).to eq(max)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
describe '#reinforce_marker' do
|
|
128
|
+
it 'reinforces an existing marker' do
|
|
129
|
+
m = store.register_marker(action: :deploy, domain: :ops, valence: 0.0)
|
|
130
|
+
result = store.reinforce_marker(marker_id: m.id, outcome_valence: 1.0)
|
|
131
|
+
expect(result).to eq(m)
|
|
132
|
+
expect(m.valence).to be > 0.0
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it 'returns nil for unknown marker id' do
|
|
136
|
+
result = store.reinforce_marker(marker_id: 'nonexistent', outcome_valence: 0.5)
|
|
137
|
+
expect(result).to be_nil
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
describe '#update_body_state' do
|
|
142
|
+
it 'updates the body state' do
|
|
143
|
+
store.update_body_state(tension: 0.9, comfort: 0.1)
|
|
144
|
+
expect(store.body_state.tension).to eq(0.9)
|
|
145
|
+
expect(store.body_state.comfort).to eq(0.1)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it 'returns the updated body state' do
|
|
149
|
+
result = store.update_body_state(gut_signal: 0.5)
|
|
150
|
+
expect(result).to be_a(Legion::Extensions::Agentic::Affect::SomaticMarker::Helpers::BodyState)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
describe '#markers_for' do
|
|
155
|
+
it 'returns markers matching action and domain' do
|
|
156
|
+
store.register_marker(action: :send, domain: :email, valence: 0.3)
|
|
157
|
+
store.register_marker(action: :send, domain: :email, valence: 0.7)
|
|
158
|
+
store.register_marker(action: :receive, domain: :email, valence: 0.5)
|
|
159
|
+
|
|
160
|
+
found = store.markers_for(action: :send, domain: :email)
|
|
161
|
+
expect(found.size).to eq(2)
|
|
162
|
+
found.each { |m| expect(m.action).to eq(:send) }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it 'returns empty array when no matching markers' do
|
|
166
|
+
expect(store.markers_for(action: :unknown, domain: :unknown)).to be_empty
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
describe '#body_influence' do
|
|
171
|
+
it 'returns composite valence and stressed flag' do
|
|
172
|
+
result = store.body_influence
|
|
173
|
+
expect(result).to have_key(:composite_valence)
|
|
174
|
+
expect(result).to have_key(:stressed)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
describe '#decay_all' do
|
|
179
|
+
it 'decays all markers' do
|
|
180
|
+
store.register_marker(action: :deploy, domain: :ops, valence: 0.5)
|
|
181
|
+
before_strength = store.markers.values.first.strength
|
|
182
|
+
store.decay_all
|
|
183
|
+
expect(store.markers.values.first.strength).to be < before_strength
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it 'removes faded markers' do
|
|
187
|
+
m = store.register_marker(action: :deploy, domain: :ops, valence: 0.0)
|
|
188
|
+
# Force marker to nearly faded state
|
|
189
|
+
50.times { m.decay }
|
|
190
|
+
store.decay_all
|
|
191
|
+
expect(store.markers).not_to have_key(m.id)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
it 'returns decay stats' do
|
|
195
|
+
store.register_marker(action: :deploy, domain: :ops, valence: 0.5)
|
|
196
|
+
result = store.decay_all
|
|
197
|
+
expect(result).to have_key(:markers_decayed)
|
|
198
|
+
expect(result).to have_key(:markers_removed)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
it 'decays body state' do
|
|
202
|
+
store.update_body_state(arousal: 0.9)
|
|
203
|
+
before = store.body_state.arousal
|
|
204
|
+
store.decay_all
|
|
205
|
+
expect(store.body_state.arousal).to be < before
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
describe '#decision_history' do
|
|
210
|
+
it 'returns up to limit recent decisions' do
|
|
211
|
+
5.times { store.decide(options: %i[act wait], domain: :ops) }
|
|
212
|
+
expect(store.decision_history(limit: 3).size).to eq(3)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
it 'returns all decisions when under limit' do
|
|
216
|
+
2.times { store.decide(options: %i[act wait], domain: :ops) }
|
|
217
|
+
expect(store.decision_history(limit: 10).size).to eq(2)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
describe '#to_h' do
|
|
222
|
+
it 'returns summary stats' do
|
|
223
|
+
store.register_marker(action: :deploy, domain: :ops, valence: 0.5)
|
|
224
|
+
store.decide(options: %i[go stop], domain: :ops)
|
|
225
|
+
|
|
226
|
+
h = store.to_h
|
|
227
|
+
expect(h[:marker_count]).to eq(1)
|
|
228
|
+
expect(h[:decision_count]).to eq(1)
|
|
229
|
+
expect(h).to have_key(:body_state)
|
|
230
|
+
expect(h).to have_key(:stressed)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::Agentic::Affect::SomaticMarker::Helpers::SomaticMarker do
|
|
4
|
+
subject(:marker) do
|
|
5
|
+
described_class.new(id: 'sm_1', action: :deploy, domain: :ops, valence: 0.5)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
describe '#initialize' do
|
|
9
|
+
it 'stores all attributes' do
|
|
10
|
+
expect(marker.id).to eq('sm_1')
|
|
11
|
+
expect(marker.action).to eq(:deploy)
|
|
12
|
+
expect(marker.domain).to eq(:ops)
|
|
13
|
+
expect(marker.strength).to eq(0.5)
|
|
14
|
+
expect(marker.source).to eq(:experience)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'clamps valence to [-1, 1]' do
|
|
18
|
+
high = described_class.new(id: 'sm_2', action: :act, domain: :d, valence: 2.0)
|
|
19
|
+
low = described_class.new(id: 'sm_3', action: :act, domain: :d, valence: -2.0)
|
|
20
|
+
expect(high.valence).to eq(1.0)
|
|
21
|
+
expect(low.valence).to eq(-1.0)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'clamps strength to [0, 1]' do
|
|
25
|
+
over = described_class.new(id: 'sm_4', action: :act, domain: :d, valence: 0.0, strength: 1.5)
|
|
26
|
+
under = described_class.new(id: 'sm_5', action: :act, domain: :d, valence: 0.0, strength: -0.5)
|
|
27
|
+
expect(over.strength).to eq(1.0)
|
|
28
|
+
expect(under.strength).to eq(0.0)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'sets created_at' do
|
|
32
|
+
expect(marker.created_at).to be_a(Time)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe '#signal' do
|
|
37
|
+
it 'returns :approach when valence > POSITIVE_BIAS' do
|
|
38
|
+
m = described_class.new(id: 'sm_6', action: :act, domain: :d, valence: 0.8)
|
|
39
|
+
expect(m.signal).to eq(:approach)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'returns :avoid when valence < NEGATIVE_BIAS' do
|
|
43
|
+
m = described_class.new(id: 'sm_7', action: :act, domain: :d, valence: -0.8)
|
|
44
|
+
expect(m.signal).to eq(:avoid)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'returns :neutral for mid-range valence' do
|
|
48
|
+
m = described_class.new(id: 'sm_8', action: :act, domain: :d, valence: 0.0)
|
|
49
|
+
expect(m.signal).to eq(:neutral)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'returns :neutral at exactly POSITIVE_BIAS boundary' do
|
|
53
|
+
m = described_class.new(id: 'sm_9', action: :act, domain: :d, valence: 0.6)
|
|
54
|
+
expect(m.signal).to eq(:neutral)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'returns :neutral at exactly NEGATIVE_BIAS boundary' do
|
|
58
|
+
m = described_class.new(id: 'sm_10', action: :act, domain: :d, valence: -0.6)
|
|
59
|
+
expect(m.signal).to eq(:neutral)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
describe '#reinforce' do
|
|
64
|
+
it 'moves valence toward positive outcome' do
|
|
65
|
+
m = described_class.new(id: 'sm_11', action: :act, domain: :d, valence: 0.0)
|
|
66
|
+
25.times { m.reinforce(outcome_valence: 1.0) }
|
|
67
|
+
expect(m.valence).to be > 0.5
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'moves valence toward negative outcome' do
|
|
71
|
+
m = described_class.new(id: 'sm_12', action: :act, domain: :d, valence: 0.0)
|
|
72
|
+
25.times { m.reinforce(outcome_valence: -1.0) }
|
|
73
|
+
expect(m.valence).to be < -0.5
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'boosts strength on reinforce' do
|
|
77
|
+
m = described_class.new(id: 'sm_13', action: :act, domain: :d, valence: 0.0, strength: 0.3)
|
|
78
|
+
m.reinforce(outcome_valence: 0.5)
|
|
79
|
+
expect(m.strength).to be > 0.3
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'clamps valence at 1.0' do
|
|
83
|
+
m = described_class.new(id: 'sm_14', action: :act, domain: :d, valence: 0.99)
|
|
84
|
+
50.times { m.reinforce(outcome_valence: 1.0) }
|
|
85
|
+
expect(m.valence).to be <= 1.0
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'clamps valence at -1.0' do
|
|
89
|
+
m = described_class.new(id: 'sm_15', action: :act, domain: :d, valence: -0.99)
|
|
90
|
+
50.times { m.reinforce(outcome_valence: -1.0) }
|
|
91
|
+
expect(m.valence).to be >= -1.0
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'clamps strength at 1.0' do
|
|
95
|
+
m = described_class.new(id: 'sm_16', action: :act, domain: :d, valence: 0.0, strength: 0.95)
|
|
96
|
+
m.reinforce(outcome_valence: 0.5)
|
|
97
|
+
expect(m.strength).to be <= 1.0
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
describe '#decay' do
|
|
102
|
+
it 'reduces strength by MARKER_DECAY' do
|
|
103
|
+
m = described_class.new(id: 'sm_17', action: :act, domain: :d, valence: 0.0, strength: 0.5)
|
|
104
|
+
before = m.strength
|
|
105
|
+
m.decay
|
|
106
|
+
expect(m.strength).to be_within(0.001).of(before - Legion::Extensions::Agentic::Affect::SomaticMarker::Helpers::Constants::MARKER_DECAY)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it 'floors strength at 0.0' do
|
|
110
|
+
m = described_class.new(id: 'sm_18', action: :act, domain: :d, valence: 0.0, strength: 0.005)
|
|
111
|
+
m.decay
|
|
112
|
+
expect(m.strength).to eq(0.0)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
describe '#faded?' do
|
|
117
|
+
it 'returns false when strength is above floor' do
|
|
118
|
+
m = described_class.new(id: 'sm_19', action: :act, domain: :d, valence: 0.0, strength: 0.5)
|
|
119
|
+
expect(m.faded?).to be false
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it 'returns true when strength is at or below floor' do
|
|
123
|
+
m = described_class.new(id: 'sm_20', action: :act, domain: :d, valence: 0.0, strength: 0.05)
|
|
124
|
+
expect(m.faded?).to be true
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it 'returns true after enough decay cycles' do
|
|
128
|
+
m = described_class.new(id: 'sm_21', action: :act, domain: :d, valence: 0.0, strength: 0.2)
|
|
129
|
+
20.times { m.decay }
|
|
130
|
+
expect(m.faded?).to be true
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
describe '#valence_label' do
|
|
135
|
+
it 'returns :strongly_negative for very negative valence' do
|
|
136
|
+
m = described_class.new(id: 'sm_22', action: :act, domain: :d, valence: -0.9)
|
|
137
|
+
expect(m.valence_label).to eq(:strongly_negative)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it 'returns :negative for moderately negative valence' do
|
|
141
|
+
m = described_class.new(id: 'sm_23', action: :act, domain: :d, valence: -0.4)
|
|
142
|
+
expect(m.valence_label).to eq(:negative)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it 'returns :neutral for near-zero valence' do
|
|
146
|
+
m = described_class.new(id: 'sm_24', action: :act, domain: :d, valence: 0.0)
|
|
147
|
+
expect(m.valence_label).to eq(:neutral)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it 'returns :positive for moderately positive valence' do
|
|
151
|
+
m = described_class.new(id: 'sm_25', action: :act, domain: :d, valence: 0.4)
|
|
152
|
+
expect(m.valence_label).to eq(:positive)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it 'returns :strongly_positive for very positive valence' do
|
|
156
|
+
m = described_class.new(id: 'sm_26', action: :act, domain: :d, valence: 0.9)
|
|
157
|
+
expect(m.valence_label).to eq(:strongly_positive)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
describe '#to_h' do
|
|
162
|
+
it 'returns a hash with all expected keys' do
|
|
163
|
+
h = marker.to_h
|
|
164
|
+
expect(h).to include(:id, :action, :domain, :valence, :strength, :source, :signal, :label, :created_at)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it 'includes the computed signal' do
|
|
168
|
+
m = described_class.new(id: 'sm_27', action: :act, domain: :d, valence: 0.8)
|
|
169
|
+
expect(m.to_h[:signal]).to eq(:approach)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|