lex-interoception 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/Gemfile +11 -0
- data/lex-interoception.gemspec +31 -0
- data/lib/legion/extensions/interoception/actors/decay.rb +41 -0
- data/lib/legion/extensions/interoception/client.rb +24 -0
- data/lib/legion/extensions/interoception/helpers/body_budget.rb +148 -0
- data/lib/legion/extensions/interoception/helpers/constants.rb +64 -0
- data/lib/legion/extensions/interoception/helpers/somatic_marker.rb +71 -0
- data/lib/legion/extensions/interoception/runners/interoception.rb +97 -0
- data/lib/legion/extensions/interoception/version.rb +9 -0
- data/lib/legion/extensions/interoception.rb +17 -0
- data/spec/legion/extensions/interoception/client_spec.rb +52 -0
- data/spec/legion/extensions/interoception/helpers/body_budget_spec.rb +178 -0
- data/spec/legion/extensions/interoception/helpers/somatic_marker_spec.rb +120 -0
- data/spec/legion/extensions/interoception/runners/interoception_spec.rb +108 -0
- data/spec/spec_helper.rb +20 -0
- metadata +77 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: dace4cb240f23845d21abe89e16f97bb73d4bc993002ccfe4ceb94e01da4d72b
|
|
4
|
+
data.tar.gz: 3bcf0cbef4062bcf3fcfb58d93e5aa2c55b24c22ec545cd9a10a48b438d52f7e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d352233f77c158c7e60869010f7f46ede5099c21eb839871ffc469df701c990e9d253d8dcf9c661155adeb8700cbb2eb0766f639e20ccfa3b77a19a068f36688
|
|
7
|
+
data.tar.gz: edee25c05cfb81956ec5052fa7e85177b8eab77ab70bf80d2a55512b7d4c0a2472b5d87d0a680f8a522d6f2a1fd3602153757b9b926457145d7d3baea7566f40
|
data/Gemfile
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/interoception/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'lex-interoception'
|
|
7
|
+
spec.version = Legion::Extensions::Interoception::VERSION
|
|
8
|
+
spec.authors = ['Esity']
|
|
9
|
+
spec.email = ['matthewdiverson@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'LEX Interoception'
|
|
12
|
+
spec.description = 'Somatic marker hypothesis for brain-modeled agentic AI — internal body-state ' \
|
|
13
|
+
'monitoring, vital signal tracking, and somatic markers that bias decisions ' \
|
|
14
|
+
'based on past outcomes associated with similar internal states.'
|
|
15
|
+
spec.homepage = 'https://github.com/LegionIO/lex-interoception'
|
|
16
|
+
spec.license = 'MIT'
|
|
17
|
+
spec.required_ruby_version = '>= 3.4'
|
|
18
|
+
|
|
19
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
20
|
+
spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-interoception'
|
|
21
|
+
spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-interoception'
|
|
22
|
+
spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-interoception'
|
|
23
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-interoception/issues'
|
|
24
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
25
|
+
|
|
26
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
27
|
+
Dir.glob('{lib,spec}/**/*') + %w[lex-interoception.gemspec Gemfile]
|
|
28
|
+
end
|
|
29
|
+
spec.require_paths = ['lib']
|
|
30
|
+
spec.add_development_dependency 'legion-gaia'
|
|
31
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/actors/every'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Interoception
|
|
8
|
+
module Actor
|
|
9
|
+
class Decay < Legion::Extensions::Actors::Every
|
|
10
|
+
def runner_class
|
|
11
|
+
Legion::Extensions::Interoception::Runners::Interoception
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def runner_function
|
|
15
|
+
'update_interoception'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def time
|
|
19
|
+
60
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def run_now?
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def use_runner?
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def check_subtask?
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def generate_task?
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/interoception/helpers/constants'
|
|
4
|
+
require 'legion/extensions/interoception/helpers/somatic_marker'
|
|
5
|
+
require 'legion/extensions/interoception/helpers/body_budget'
|
|
6
|
+
require 'legion/extensions/interoception/runners/interoception'
|
|
7
|
+
|
|
8
|
+
module Legion
|
|
9
|
+
module Extensions
|
|
10
|
+
module Interoception
|
|
11
|
+
class Client
|
|
12
|
+
include Runners::Interoception
|
|
13
|
+
|
|
14
|
+
def initialize(body_budget: nil, **)
|
|
15
|
+
@body_budget = body_budget || Helpers::BodyBudget.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
attr_reader :body_budget
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Interoception
|
|
6
|
+
module Helpers
|
|
7
|
+
class BodyBudget
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
attr_reader :vitals, :baselines, :markers, :vital_history
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@vitals = {}
|
|
14
|
+
@baselines = {}
|
|
15
|
+
@markers = []
|
|
16
|
+
@vital_history = {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# --- Vital Signal Tracking ---
|
|
20
|
+
|
|
21
|
+
def report_vital(channel:, value:)
|
|
22
|
+
channel = channel.to_sym
|
|
23
|
+
normalized = value.clamp(0.0, 1.0)
|
|
24
|
+
@baselines[channel] ||= DEFAULT_BASELINE
|
|
25
|
+
@vitals[channel] = if @vitals.key?(channel)
|
|
26
|
+
ema(@vitals[channel], normalized, VITAL_ALPHA)
|
|
27
|
+
else
|
|
28
|
+
normalized
|
|
29
|
+
end
|
|
30
|
+
record_vital_history(channel, @vitals[channel])
|
|
31
|
+
@baselines[channel] = ema(@baselines[channel], @vitals[channel], VITAL_ALPHA * 0.5)
|
|
32
|
+
@vitals[channel]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def vital_for(channel)
|
|
36
|
+
@vitals.fetch(channel.to_sym, DEFAULT_BASELINE)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def deviation_for(channel)
|
|
40
|
+
channel = channel.to_sym
|
|
41
|
+
current = @vitals.fetch(channel, DEFAULT_BASELINE)
|
|
42
|
+
baseline = @baselines.fetch(channel, DEFAULT_BASELINE)
|
|
43
|
+
current - baseline
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def vital_label(channel)
|
|
47
|
+
health = vital_health(channel)
|
|
48
|
+
VITAL_LABELS.each { |range, lbl| return lbl if range.cover?(health) }
|
|
49
|
+
:nominal
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def vital_health(channel)
|
|
53
|
+
val = vital_for(channel)
|
|
54
|
+
inverted_channels = %i[cpu_load memory_pressure queue_depth error_rate disk_usage gc_pressure]
|
|
55
|
+
inverted_channels.include?(channel.to_sym) ? 1.0 - val : val
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def deviating_channels
|
|
59
|
+
@vitals.select { |ch, _| deviation_for(ch).abs >= DEVIATION_THRESHOLD }
|
|
60
|
+
.map { |ch, _| { channel: ch, deviation: deviation_for(ch).round(4), label: vital_label(ch) } }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# --- Somatic Markers ---
|
|
64
|
+
|
|
65
|
+
def create_marker(action:, domain:, valence:, strength: 1.0)
|
|
66
|
+
marker = SomaticMarker.new(action: action, domain: domain, valence: valence, strength: strength)
|
|
67
|
+
@markers << marker
|
|
68
|
+
prune_markers if @markers.size > MAX_MARKERS
|
|
69
|
+
marker
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def markers_for(action:, domain: nil)
|
|
73
|
+
results = @markers.select { |m| m.action == action }
|
|
74
|
+
results = results.select { |m| m.domain == domain } if domain
|
|
75
|
+
results
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def bias_for_action(action:, domain: nil)
|
|
79
|
+
relevant = markers_for(action: action, domain: domain)
|
|
80
|
+
return 0.0 if relevant.empty?
|
|
81
|
+
|
|
82
|
+
relevant.sum { |m| m.bias_for(action) } / relevant.size
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def reinforce_markers(action:, domain: nil, amount: 0.1)
|
|
86
|
+
markers_for(action: action, domain: domain).each { |m| m.reinforce(amount: amount) }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def decay_markers
|
|
90
|
+
@markers.each(&:decay)
|
|
91
|
+
@markers.reject!(&:faded?)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# --- Body Budget Overview ---
|
|
95
|
+
|
|
96
|
+
def overall_health
|
|
97
|
+
return DEFAULT_BASELINE if @vitals.empty?
|
|
98
|
+
|
|
99
|
+
healths = @vitals.keys.map { |ch| vital_health(ch) }
|
|
100
|
+
healths.sum / healths.size
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def body_budget_label
|
|
104
|
+
health = overall_health
|
|
105
|
+
BODY_BUDGET_LABELS.each { |range, lbl| return lbl if range.cover?(health) }
|
|
106
|
+
:comfortable
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def channel_count
|
|
110
|
+
@vitals.size
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def marker_count
|
|
114
|
+
@markers.size
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def to_h
|
|
118
|
+
{
|
|
119
|
+
overall_health: overall_health.round(4),
|
|
120
|
+
body_budget_label: body_budget_label,
|
|
121
|
+
channels: channel_count,
|
|
122
|
+
markers: marker_count,
|
|
123
|
+
vitals: @vitals.transform_values { |v| v.round(4) },
|
|
124
|
+
deviations: deviating_channels
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
def ema(old_val, new_val, alpha)
|
|
131
|
+
old_val + (alpha * (new_val - old_val))
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def record_vital_history(channel, value)
|
|
135
|
+
@vital_history[channel] ||= []
|
|
136
|
+
@vital_history[channel] << { value: value, at: Time.now.utc }
|
|
137
|
+
@vital_history[channel].shift while @vital_history[channel].size > MAX_VITAL_HISTORY
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def prune_markers
|
|
141
|
+
@markers.sort_by!(&:strength)
|
|
142
|
+
@markers.shift while @markers.size > MAX_MARKERS
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Interoception
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
# Vital signal channels the agent monitors
|
|
9
|
+
VITAL_CHANNELS = %i[
|
|
10
|
+
cpu_load memory_pressure queue_depth
|
|
11
|
+
response_latency error_rate connection_health
|
|
12
|
+
disk_usage thread_count gc_pressure
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
# Somatic marker valence thresholds
|
|
16
|
+
MARKER_POSITIVE_THRESHOLD = 0.3
|
|
17
|
+
MARKER_NEGATIVE_THRESHOLD = -0.3
|
|
18
|
+
|
|
19
|
+
# How strongly markers bias decisions (0..1)
|
|
20
|
+
MARKER_INFLUENCE = 0.4
|
|
21
|
+
|
|
22
|
+
# EMA alpha for vital signal smoothing
|
|
23
|
+
VITAL_ALPHA = 0.15
|
|
24
|
+
|
|
25
|
+
# Default baseline for vitals (normalized 0..1)
|
|
26
|
+
DEFAULT_BASELINE = 0.5
|
|
27
|
+
|
|
28
|
+
# Deviation from baseline that triggers a somatic marker
|
|
29
|
+
DEVIATION_THRESHOLD = 0.2
|
|
30
|
+
|
|
31
|
+
# Maximum stored somatic markers
|
|
32
|
+
MAX_MARKERS = 200
|
|
33
|
+
|
|
34
|
+
# Maximum stored vital snapshots per channel
|
|
35
|
+
MAX_VITAL_HISTORY = 100
|
|
36
|
+
|
|
37
|
+
# Marker decay per tick
|
|
38
|
+
MARKER_DECAY = 0.02
|
|
39
|
+
|
|
40
|
+
# Marker floor (below this, marker is pruned)
|
|
41
|
+
MARKER_FLOOR = 0.05
|
|
42
|
+
|
|
43
|
+
# Body budget labels based on overall vital health
|
|
44
|
+
BODY_BUDGET_LABELS = {
|
|
45
|
+
(0.8..) => :thriving,
|
|
46
|
+
(0.6...0.8) => :comfortable,
|
|
47
|
+
(0.4...0.6) => :strained,
|
|
48
|
+
(0.2...0.4) => :distressed,
|
|
49
|
+
(..0.2) => :critical
|
|
50
|
+
}.freeze
|
|
51
|
+
|
|
52
|
+
# Vital health labels
|
|
53
|
+
VITAL_LABELS = {
|
|
54
|
+
(0.8..) => :healthy,
|
|
55
|
+
(0.6...0.8) => :nominal,
|
|
56
|
+
(0.4...0.6) => :elevated,
|
|
57
|
+
(0.2...0.4) => :warning,
|
|
58
|
+
(..0.2) => :critical
|
|
59
|
+
}.freeze
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Interoception
|
|
6
|
+
module Helpers
|
|
7
|
+
class SomaticMarker
|
|
8
|
+
attr_reader :id, :action, :domain, :valence, :created_at
|
|
9
|
+
attr_accessor :strength
|
|
10
|
+
|
|
11
|
+
def initialize(action:, domain:, valence:, strength: 1.0)
|
|
12
|
+
@id = SecureRandom.uuid
|
|
13
|
+
@action = action
|
|
14
|
+
@domain = domain
|
|
15
|
+
@valence = valence.clamp(-1.0, 1.0)
|
|
16
|
+
@strength = strength.clamp(0.0, 1.0)
|
|
17
|
+
@created_at = Time.now.utc
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def bias_for(candidate_action)
|
|
21
|
+
return 0.0 unless candidate_action == @action
|
|
22
|
+
|
|
23
|
+
@valence * @strength * Constants::MARKER_INFLUENCE
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def reinforce(amount: 0.1)
|
|
27
|
+
@strength = [@strength + amount, 1.0].min
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def decay
|
|
31
|
+
@strength = [@strength - Constants::MARKER_DECAY, Constants::MARKER_FLOOR].max
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def faded?
|
|
35
|
+
@strength <= Constants::MARKER_FLOOR
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def positive?
|
|
39
|
+
@valence >= Constants::MARKER_POSITIVE_THRESHOLD
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def negative?
|
|
43
|
+
@valence <= Constants::MARKER_NEGATIVE_THRESHOLD
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def label
|
|
47
|
+
if positive?
|
|
48
|
+
:approach
|
|
49
|
+
elsif negative?
|
|
50
|
+
:avoid
|
|
51
|
+
else
|
|
52
|
+
:neutral
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def to_h
|
|
57
|
+
{
|
|
58
|
+
id: @id,
|
|
59
|
+
action: @action,
|
|
60
|
+
domain: @domain,
|
|
61
|
+
valence: @valence,
|
|
62
|
+
strength: @strength,
|
|
63
|
+
label: label,
|
|
64
|
+
created_at: @created_at
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Interoception
|
|
6
|
+
module Runners
|
|
7
|
+
module Interoception
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
def report_vital(channel:, value:, **)
|
|
12
|
+
smoothed = body_budget.report_vital(channel: channel, value: value.to_f)
|
|
13
|
+
deviation = body_budget.deviation_for(channel)
|
|
14
|
+
Legion::Logging.debug "[interoception] vital: channel=#{channel} raw=#{value} " \
|
|
15
|
+
"smoothed=#{smoothed.round(3)} deviation=#{deviation.round(3)}"
|
|
16
|
+
{
|
|
17
|
+
success: true,
|
|
18
|
+
channel: channel,
|
|
19
|
+
smoothed: smoothed.round(4),
|
|
20
|
+
deviation: deviation.round(4),
|
|
21
|
+
label: body_budget.vital_label(channel)
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def create_somatic_marker(action:, domain:, valence:, strength: 1.0, **)
|
|
26
|
+
marker = body_budget.create_marker(action: action, domain: domain, valence: valence.to_f, strength: strength.to_f)
|
|
27
|
+
Legion::Logging.debug "[interoception] marker: action=#{action} domain=#{domain} " \
|
|
28
|
+
"valence=#{marker.valence.round(2)} label=#{marker.label}"
|
|
29
|
+
{ success: true, marker: marker.to_h }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def query_bias(action:, domain: nil, **)
|
|
33
|
+
bias = body_budget.bias_for_action(action: action, domain: domain)
|
|
34
|
+
label = if bias > Helpers::Constants::MARKER_POSITIVE_THRESHOLD * Helpers::Constants::MARKER_INFLUENCE
|
|
35
|
+
:approach
|
|
36
|
+
elsif bias < Helpers::Constants::MARKER_NEGATIVE_THRESHOLD * Helpers::Constants::MARKER_INFLUENCE
|
|
37
|
+
:avoid
|
|
38
|
+
else
|
|
39
|
+
:neutral
|
|
40
|
+
end
|
|
41
|
+
Legion::Logging.debug "[interoception] bias: action=#{action} domain=#{domain} bias=#{bias.round(3)} label=#{label}"
|
|
42
|
+
{ success: true, action: action, domain: domain, bias: bias.round(4), label: label }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def reinforce_somatic(action:, domain: nil, amount: 0.1, **)
|
|
46
|
+
body_budget.reinforce_markers(action: action, domain: domain, amount: amount.to_f)
|
|
47
|
+
Legion::Logging.debug "[interoception] reinforce: action=#{action} domain=#{domain} amount=#{amount}"
|
|
48
|
+
{ success: true, action: action, domain: domain }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def deviating_vitals(**)
|
|
52
|
+
deviations = body_budget.deviating_channels
|
|
53
|
+
Legion::Logging.debug "[interoception] deviating: count=#{deviations.size}"
|
|
54
|
+
{ success: true, deviations: deviations, count: deviations.size }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def body_status(**)
|
|
58
|
+
health = body_budget.overall_health
|
|
59
|
+
label = body_budget.body_budget_label
|
|
60
|
+
Legion::Logging.debug "[interoception] status: health=#{health.round(3)} label=#{label}"
|
|
61
|
+
{
|
|
62
|
+
success: true,
|
|
63
|
+
health: health.round(4),
|
|
64
|
+
label: label,
|
|
65
|
+
channels: body_budget.channel_count,
|
|
66
|
+
markers: body_budget.marker_count
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def update_interoception(**)
|
|
71
|
+
body_budget.decay_markers
|
|
72
|
+
health = body_budget.overall_health
|
|
73
|
+
Legion::Logging.debug "[interoception] tick: health=#{health.round(3)} " \
|
|
74
|
+
"channels=#{body_budget.channel_count} markers=#{body_budget.marker_count}"
|
|
75
|
+
{
|
|
76
|
+
success: true,
|
|
77
|
+
health: health.round(4),
|
|
78
|
+
label: body_budget.body_budget_label,
|
|
79
|
+
channels: body_budget.channel_count,
|
|
80
|
+
markers: body_budget.marker_count
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def interoception_stats(**)
|
|
85
|
+
{ success: true, stats: body_budget.to_h }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def body_budget
|
|
91
|
+
@body_budget ||= Helpers::BodyBudget.new
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require 'legion/extensions/interoception/version'
|
|
5
|
+
require 'legion/extensions/interoception/helpers/constants'
|
|
6
|
+
require 'legion/extensions/interoception/helpers/somatic_marker'
|
|
7
|
+
require 'legion/extensions/interoception/helpers/body_budget'
|
|
8
|
+
require 'legion/extensions/interoception/runners/interoception'
|
|
9
|
+
require 'legion/extensions/interoception/client'
|
|
10
|
+
|
|
11
|
+
module Legion
|
|
12
|
+
module Extensions
|
|
13
|
+
module Interoception
|
|
14
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::Interoception::Client do
|
|
4
|
+
subject(:client) { described_class.new }
|
|
5
|
+
|
|
6
|
+
it 'includes Runners::Interoception' do
|
|
7
|
+
expect(described_class.ancestors).to include(Legion::Extensions::Interoception::Runners::Interoception)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it 'responds to all runner methods' do
|
|
11
|
+
expect(client).to respond_to(:report_vital)
|
|
12
|
+
expect(client).to respond_to(:create_somatic_marker)
|
|
13
|
+
expect(client).to respond_to(:query_bias)
|
|
14
|
+
expect(client).to respond_to(:reinforce_somatic)
|
|
15
|
+
expect(client).to respond_to(:deviating_vitals)
|
|
16
|
+
expect(client).to respond_to(:body_status)
|
|
17
|
+
expect(client).to respond_to(:update_interoception)
|
|
18
|
+
expect(client).to respond_to(:interoception_stats)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'supports full somatic marker lifecycle' do
|
|
22
|
+
# Report some vitals
|
|
23
|
+
client.report_vital(channel: :cpu_load, value: 0.3)
|
|
24
|
+
client.report_vital(channel: :connection_health, value: 0.9)
|
|
25
|
+
|
|
26
|
+
# Create markers based on outcomes
|
|
27
|
+
client.create_somatic_marker(action: :deploy, domain: :prod, valence: 0.8)
|
|
28
|
+
client.create_somatic_marker(action: :risky_change, domain: :prod, valence: -0.6)
|
|
29
|
+
|
|
30
|
+
# Query bias should reflect markers
|
|
31
|
+
approach = client.query_bias(action: :deploy)
|
|
32
|
+
expect(approach[:bias]).to be > 0
|
|
33
|
+
|
|
34
|
+
avoid = client.query_bias(action: :risky_change)
|
|
35
|
+
expect(avoid[:bias]).to be < 0
|
|
36
|
+
|
|
37
|
+
# Reinforce good outcome
|
|
38
|
+
client.reinforce_somatic(action: :deploy)
|
|
39
|
+
|
|
40
|
+
# Tick decay
|
|
41
|
+
client.update_interoception
|
|
42
|
+
|
|
43
|
+
# Check overall status
|
|
44
|
+
status = client.body_status
|
|
45
|
+
expect(status[:success]).to be true
|
|
46
|
+
expect(status[:health]).to be > 0
|
|
47
|
+
|
|
48
|
+
stats = client.interoception_stats
|
|
49
|
+
expect(stats[:stats][:channels]).to eq(2)
|
|
50
|
+
expect(stats[:stats][:markers]).to be >= 1
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::Interoception::Helpers::BodyBudget do
|
|
4
|
+
subject(:budget) { described_class.new }
|
|
5
|
+
|
|
6
|
+
describe '#report_vital' do
|
|
7
|
+
it 'stores and smooths a vital signal' do
|
|
8
|
+
result = budget.report_vital(channel: :cpu_load, value: 0.7)
|
|
9
|
+
expect(result).to be_a(Float)
|
|
10
|
+
expect(result).to be_between(0.0, 1.0)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'applies EMA smoothing on repeated reports' do
|
|
14
|
+
budget.report_vital(channel: :cpu_load, value: 0.2)
|
|
15
|
+
budget.report_vital(channel: :cpu_load, value: 0.8)
|
|
16
|
+
val = budget.vital_for(:cpu_load)
|
|
17
|
+
expect(val).to be_between(0.2, 0.8)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'clamps values to 0..1' do
|
|
21
|
+
budget.report_vital(channel: :cpu_load, value: 5.0)
|
|
22
|
+
expect(budget.vital_for(:cpu_load)).to be <= 1.0
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'updates baseline over time' do
|
|
26
|
+
10.times { budget.report_vital(channel: :cpu_load, value: 0.9) }
|
|
27
|
+
expect(budget.baselines[:cpu_load]).to be > 0.5
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe '#deviation_for' do
|
|
32
|
+
it 'returns 0 for new channels' do
|
|
33
|
+
expect(budget.deviation_for(:cpu_load)).to eq(0.0)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'detects deviation from baseline' do
|
|
37
|
+
5.times { budget.report_vital(channel: :cpu_load, value: 0.3) }
|
|
38
|
+
budget.report_vital(channel: :cpu_load, value: 0.9)
|
|
39
|
+
expect(budget.deviation_for(:cpu_load).abs).to be > 0
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe '#vital_label' do
|
|
44
|
+
it 'returns :healthy for healthy vitals' do
|
|
45
|
+
budget.report_vital(channel: :connection_health, value: 0.9)
|
|
46
|
+
expect(budget.vital_label(:connection_health)).to eq(:healthy)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'returns :critical for bad inverted vitals' do
|
|
50
|
+
budget.report_vital(channel: :cpu_load, value: 0.95)
|
|
51
|
+
expect(budget.vital_label(:cpu_load)).to eq(:critical)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
describe '#vital_health' do
|
|
56
|
+
it 'inverts cpu_load (high load = low health)' do
|
|
57
|
+
budget.report_vital(channel: :cpu_load, value: 0.9)
|
|
58
|
+
expect(budget.vital_health(:cpu_load)).to be < 0.2
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'keeps connection_health direct (high = good)' do
|
|
62
|
+
budget.report_vital(channel: :connection_health, value: 0.9)
|
|
63
|
+
expect(budget.vital_health(:connection_health)).to be > 0.8
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
describe '#deviating_channels' do
|
|
68
|
+
it 'returns empty when no deviations' do
|
|
69
|
+
expect(budget.deviating_channels).to be_empty
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'detects channels with significant deviation' do
|
|
73
|
+
5.times { budget.report_vital(channel: :cpu_load, value: 0.2) }
|
|
74
|
+
# Force a big jump
|
|
75
|
+
budget.report_vital(channel: :cpu_load, value: 0.95)
|
|
76
|
+
budget.report_vital(channel: :cpu_load, value: 0.95)
|
|
77
|
+
budget.report_vital(channel: :cpu_load, value: 0.95)
|
|
78
|
+
deviations = budget.deviating_channels
|
|
79
|
+
expect(deviations.first[:channel]).to eq(:cpu_load) if deviations.any?
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
describe 'somatic markers' do
|
|
84
|
+
describe '#create_marker' do
|
|
85
|
+
it 'creates and stores a marker' do
|
|
86
|
+
marker = budget.create_marker(action: :deploy, domain: :prod, valence: 0.7)
|
|
87
|
+
expect(marker).to be_a(Legion::Extensions::Interoception::Helpers::SomaticMarker)
|
|
88
|
+
expect(budget.marker_count).to eq(1)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
describe '#markers_for' do
|
|
93
|
+
it 'finds markers by action' do
|
|
94
|
+
budget.create_marker(action: :deploy, domain: :prod, valence: 0.5)
|
|
95
|
+
budget.create_marker(action: :deploy, domain: :staging, valence: 0.3)
|
|
96
|
+
budget.create_marker(action: :rollback, domain: :prod, valence: -0.5)
|
|
97
|
+
expect(budget.markers_for(action: :deploy).size).to eq(2)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'filters by domain' do
|
|
101
|
+
budget.create_marker(action: :deploy, domain: :prod, valence: 0.5)
|
|
102
|
+
budget.create_marker(action: :deploy, domain: :staging, valence: 0.3)
|
|
103
|
+
expect(budget.markers_for(action: :deploy, domain: :prod).size).to eq(1)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
describe '#bias_for_action' do
|
|
108
|
+
it 'returns 0.0 with no markers' do
|
|
109
|
+
expect(budget.bias_for_action(action: :deploy)).to eq(0.0)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'returns positive bias for approach markers' do
|
|
113
|
+
budget.create_marker(action: :deploy, domain: :prod, valence: 0.8)
|
|
114
|
+
expect(budget.bias_for_action(action: :deploy)).to be > 0
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'returns negative bias for avoid markers' do
|
|
118
|
+
budget.create_marker(action: :deploy, domain: :prod, valence: -0.8)
|
|
119
|
+
expect(budget.bias_for_action(action: :deploy)).to be < 0
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
describe '#reinforce_markers' do
|
|
124
|
+
it 'increases strength of matching markers' do
|
|
125
|
+
budget.create_marker(action: :deploy, domain: :prod, valence: 0.5, strength: 0.4)
|
|
126
|
+
budget.reinforce_markers(action: :deploy, amount: 0.2)
|
|
127
|
+
marker = budget.markers_for(action: :deploy).first
|
|
128
|
+
expect(marker.strength).to be_within(0.001).of(0.6)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
describe '#decay_markers' do
|
|
133
|
+
it 'decays all markers' do
|
|
134
|
+
budget.create_marker(action: :deploy, domain: :prod, valence: 0.5)
|
|
135
|
+
before = budget.markers_for(action: :deploy).first.strength
|
|
136
|
+
budget.decay_markers
|
|
137
|
+
after = budget.markers_for(action: :deploy).first&.strength
|
|
138
|
+
expect(after).to be < before if after
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it 'prunes faded markers' do
|
|
142
|
+
floor = Legion::Extensions::Interoception::Helpers::Constants::MARKER_FLOOR
|
|
143
|
+
budget.create_marker(action: :deploy, domain: :prod, valence: 0.5, strength: floor + 0.01)
|
|
144
|
+
budget.decay_markers
|
|
145
|
+
expect(budget.marker_count).to eq(0)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
describe '#overall_health' do
|
|
151
|
+
it 'returns DEFAULT_BASELINE with no vitals' do
|
|
152
|
+
expect(budget.overall_health).to eq(Legion::Extensions::Interoception::Helpers::Constants::DEFAULT_BASELINE)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it 'computes average health across channels' do
|
|
156
|
+
budget.report_vital(channel: :connection_health, value: 0.9)
|
|
157
|
+
budget.report_vital(channel: :cpu_load, value: 0.1) # low load = healthy
|
|
158
|
+
expect(budget.overall_health).to be > 0.7
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
describe '#body_budget_label' do
|
|
163
|
+
it 'returns :comfortable for healthy vitals' do
|
|
164
|
+
budget.report_vital(channel: :connection_health, value: 0.7)
|
|
165
|
+
label = budget.body_budget_label
|
|
166
|
+
expect(%i[thriving comfortable]).to include(label)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
describe '#to_h' do
|
|
171
|
+
it 'returns comprehensive stats' do
|
|
172
|
+
budget.report_vital(channel: :cpu_load, value: 0.3)
|
|
173
|
+
budget.create_marker(action: :deploy, domain: :prod, valence: 0.5)
|
|
174
|
+
h = budget.to_h
|
|
175
|
+
expect(h).to include(:overall_health, :body_budget_label, :channels, :markers, :vitals, :deviations)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::Interoception::Helpers::SomaticMarker do
|
|
4
|
+
subject(:marker) { described_class.new(action: :deploy, domain: :production, valence: 0.6) }
|
|
5
|
+
|
|
6
|
+
describe '#initialize' do
|
|
7
|
+
it 'assigns fields' do
|
|
8
|
+
expect(marker.action).to eq(:deploy)
|
|
9
|
+
expect(marker.domain).to eq(:production)
|
|
10
|
+
expect(marker.valence).to eq(0.6)
|
|
11
|
+
expect(marker.strength).to eq(1.0)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'assigns uuid and timestamp' do
|
|
15
|
+
expect(marker.id).to match(/\A[0-9a-f-]{36}\z/)
|
|
16
|
+
expect(marker.created_at).to be_a(Time)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'clamps valence to -1.0..1.0' do
|
|
20
|
+
high = described_class.new(action: :a, domain: :d, valence: 2.0)
|
|
21
|
+
low = described_class.new(action: :a, domain: :d, valence: -2.0)
|
|
22
|
+
expect(high.valence).to eq(1.0)
|
|
23
|
+
expect(low.valence).to eq(-1.0)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'clamps strength to 0.0..1.0' do
|
|
27
|
+
high = described_class.new(action: :a, domain: :d, valence: 0.5, strength: 5.0)
|
|
28
|
+
expect(high.strength).to eq(1.0)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe '#bias_for' do
|
|
33
|
+
it 'returns positive bias for matching action with positive valence' do
|
|
34
|
+
bias = marker.bias_for(:deploy)
|
|
35
|
+
expect(bias).to be > 0
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'returns 0 for non-matching action' do
|
|
39
|
+
expect(marker.bias_for(:rollback)).to eq(0.0)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'applies MARKER_INFLUENCE factor' do
|
|
43
|
+
influence = Legion::Extensions::Interoception::Helpers::Constants::MARKER_INFLUENCE
|
|
44
|
+
expected = marker.valence * marker.strength * influence
|
|
45
|
+
expect(marker.bias_for(:deploy)).to be_within(0.001).of(expected)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe '#reinforce' do
|
|
50
|
+
it 'increases strength' do
|
|
51
|
+
marker.strength = 0.5
|
|
52
|
+
marker.reinforce(amount: 0.2)
|
|
53
|
+
expect(marker.strength).to be_within(0.001).of(0.7)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'caps at 1.0' do
|
|
57
|
+
marker.reinforce(amount: 0.5)
|
|
58
|
+
expect(marker.strength).to eq(1.0)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
describe '#decay' do
|
|
63
|
+
it 'reduces strength by MARKER_DECAY' do
|
|
64
|
+
before = marker.strength
|
|
65
|
+
marker.decay
|
|
66
|
+
decay_amount = Legion::Extensions::Interoception::Helpers::Constants::MARKER_DECAY
|
|
67
|
+
expect(marker.strength).to be_within(0.001).of(before - decay_amount)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'does not drop below MARKER_FLOOR' do
|
|
71
|
+
50.times { marker.decay }
|
|
72
|
+
expect(marker.strength).to be >= Legion::Extensions::Interoception::Helpers::Constants::MARKER_FLOOR
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
describe '#faded?' do
|
|
77
|
+
it 'returns false for strong markers' do
|
|
78
|
+
expect(marker.faded?).to be false
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'returns true at or below floor' do
|
|
82
|
+
marker.strength = Legion::Extensions::Interoception::Helpers::Constants::MARKER_FLOOR
|
|
83
|
+
expect(marker.faded?).to be true
|
|
84
|
+
|
|
85
|
+
marker.strength = Legion::Extensions::Interoception::Helpers::Constants::MARKER_FLOOR + 0.01
|
|
86
|
+
expect(marker.faded?).to be false
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
describe '#positive? / #negative? / #label' do
|
|
91
|
+
it 'returns :approach for positive valence' do
|
|
92
|
+
pos = described_class.new(action: :a, domain: :d, valence: 0.5)
|
|
93
|
+
expect(pos.positive?).to be true
|
|
94
|
+
expect(pos.negative?).to be false
|
|
95
|
+
expect(pos.label).to eq(:approach)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'returns :avoid for negative valence' do
|
|
99
|
+
neg = described_class.new(action: :a, domain: :d, valence: -0.5)
|
|
100
|
+
expect(neg.positive?).to be false
|
|
101
|
+
expect(neg.negative?).to be true
|
|
102
|
+
expect(neg.label).to eq(:avoid)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it 'returns :neutral for middle valence' do
|
|
106
|
+
mid = described_class.new(action: :a, domain: :d, valence: 0.0)
|
|
107
|
+
expect(mid.positive?).to be false
|
|
108
|
+
expect(mid.negative?).to be false
|
|
109
|
+
expect(mid.label).to eq(:neutral)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
describe '#to_h' do
|
|
114
|
+
it 'returns a hash with all fields' do
|
|
115
|
+
h = marker.to_h
|
|
116
|
+
expect(h).to include(:id, :action, :domain, :valence, :strength, :label, :created_at)
|
|
117
|
+
expect(h[:action]).to eq(:deploy)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::Interoception::Runners::Interoception do
|
|
4
|
+
let(:client) { Legion::Extensions::Interoception::Client.new }
|
|
5
|
+
|
|
6
|
+
describe '#report_vital' do
|
|
7
|
+
it 'reports and smooths a vital signal' do
|
|
8
|
+
result = client.report_vital(channel: :cpu_load, value: 0.6)
|
|
9
|
+
expect(result[:success]).to be true
|
|
10
|
+
expect(result[:channel]).to eq(:cpu_load)
|
|
11
|
+
expect(result[:smoothed]).to be_a(Float)
|
|
12
|
+
expect(result).to have_key(:deviation)
|
|
13
|
+
expect(result).to have_key(:label)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'tracks deviation from baseline' do
|
|
17
|
+
5.times { client.report_vital(channel: :cpu_load, value: 0.2) }
|
|
18
|
+
result = client.report_vital(channel: :cpu_load, value: 0.9)
|
|
19
|
+
expect(result[:deviation].abs).to be > 0
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe '#create_somatic_marker' do
|
|
24
|
+
it 'creates a marker with correct fields' do
|
|
25
|
+
result = client.create_somatic_marker(action: :deploy, domain: :prod, valence: 0.7)
|
|
26
|
+
expect(result[:success]).to be true
|
|
27
|
+
expect(result[:marker][:action]).to eq(:deploy)
|
|
28
|
+
expect(result[:marker][:valence]).to eq(0.7)
|
|
29
|
+
expect(result[:marker][:label]).to eq(:approach)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'creates negative marker' do
|
|
33
|
+
result = client.create_somatic_marker(action: :delete_data, domain: :prod, valence: -0.8)
|
|
34
|
+
expect(result[:marker][:label]).to eq(:avoid)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe '#query_bias' do
|
|
39
|
+
it 'returns neutral with no markers' do
|
|
40
|
+
result = client.query_bias(action: :deploy)
|
|
41
|
+
expect(result[:success]).to be true
|
|
42
|
+
expect(result[:bias]).to eq(0.0)
|
|
43
|
+
expect(result[:label]).to eq(:neutral)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'returns approach bias for positive markers' do
|
|
47
|
+
client.create_somatic_marker(action: :deploy, domain: :prod, valence: 0.8)
|
|
48
|
+
result = client.query_bias(action: :deploy)
|
|
49
|
+
expect(result[:bias]).to be > 0
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'returns avoid bias for negative markers' do
|
|
53
|
+
client.create_somatic_marker(action: :risky_change, domain: :prod, valence: -0.9)
|
|
54
|
+
result = client.query_bias(action: :risky_change)
|
|
55
|
+
expect(result[:bias]).to be < 0
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
describe '#reinforce_somatic' do
|
|
60
|
+
it 'reinforces matching markers' do
|
|
61
|
+
client.create_somatic_marker(action: :deploy, domain: :prod, valence: 0.5, strength: 0.3)
|
|
62
|
+
result = client.reinforce_somatic(action: :deploy, amount: 0.2)
|
|
63
|
+
expect(result[:success]).to be true
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
describe '#deviating_vitals' do
|
|
68
|
+
it 'returns deviating channels' do
|
|
69
|
+
result = client.deviating_vitals
|
|
70
|
+
expect(result[:success]).to be true
|
|
71
|
+
expect(result[:deviations]).to be_an(Array)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
describe '#body_status' do
|
|
76
|
+
it 'returns overall health status' do
|
|
77
|
+
client.report_vital(channel: :cpu_load, value: 0.2)
|
|
78
|
+
result = client.body_status
|
|
79
|
+
expect(result[:success]).to be true
|
|
80
|
+
expect(result[:health]).to be_a(Float)
|
|
81
|
+
expect(result).to have_key(:label)
|
|
82
|
+
expect(result).to have_key(:channels)
|
|
83
|
+
expect(result).to have_key(:markers)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
describe '#update_interoception' do
|
|
88
|
+
it 'decays markers and returns status' do
|
|
89
|
+
client.create_somatic_marker(action: :deploy, domain: :prod, valence: 0.5)
|
|
90
|
+
result = client.update_interoception
|
|
91
|
+
expect(result[:success]).to be true
|
|
92
|
+
expect(result).to have_key(:health)
|
|
93
|
+
expect(result).to have_key(:label)
|
|
94
|
+
expect(result).to have_key(:channels)
|
|
95
|
+
expect(result).to have_key(:markers)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
describe '#interoception_stats' do
|
|
100
|
+
it 'returns comprehensive stats' do
|
|
101
|
+
client.report_vital(channel: :cpu_load, value: 0.4)
|
|
102
|
+
client.create_somatic_marker(action: :deploy, domain: :prod, valence: 0.7)
|
|
103
|
+
result = client.interoception_stats
|
|
104
|
+
expect(result[:success]).to be true
|
|
105
|
+
expect(result[:stats]).to include(:overall_health, :body_budget_label, :channels, :markers)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Logging
|
|
7
|
+
def self.debug(_msg); end
|
|
8
|
+
def self.info(_msg); end
|
|
9
|
+
def self.warn(_msg); end
|
|
10
|
+
def self.error(_msg); end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
require 'legion/extensions/interoception'
|
|
15
|
+
|
|
16
|
+
RSpec.configure do |config|
|
|
17
|
+
config.example_status_persistence_file_path = '.rspec_status'
|
|
18
|
+
config.disable_monkey_patching!
|
|
19
|
+
config.expect_with(:rspec) { |c| c.syntax = :expect }
|
|
20
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-interoception
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Esity
|
|
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: Somatic marker hypothesis for brain-modeled agentic AI — internal body-state
|
|
27
|
+
monitoring, vital signal tracking, and somatic markers that bias decisions based
|
|
28
|
+
on past outcomes associated with similar internal states.
|
|
29
|
+
email:
|
|
30
|
+
- matthewdiverson@gmail.com
|
|
31
|
+
executables: []
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- Gemfile
|
|
36
|
+
- lex-interoception.gemspec
|
|
37
|
+
- lib/legion/extensions/interoception.rb
|
|
38
|
+
- lib/legion/extensions/interoception/actors/decay.rb
|
|
39
|
+
- lib/legion/extensions/interoception/client.rb
|
|
40
|
+
- lib/legion/extensions/interoception/helpers/body_budget.rb
|
|
41
|
+
- lib/legion/extensions/interoception/helpers/constants.rb
|
|
42
|
+
- lib/legion/extensions/interoception/helpers/somatic_marker.rb
|
|
43
|
+
- lib/legion/extensions/interoception/runners/interoception.rb
|
|
44
|
+
- lib/legion/extensions/interoception/version.rb
|
|
45
|
+
- spec/legion/extensions/interoception/client_spec.rb
|
|
46
|
+
- spec/legion/extensions/interoception/helpers/body_budget_spec.rb
|
|
47
|
+
- spec/legion/extensions/interoception/helpers/somatic_marker_spec.rb
|
|
48
|
+
- spec/legion/extensions/interoception/runners/interoception_spec.rb
|
|
49
|
+
- spec/spec_helper.rb
|
|
50
|
+
homepage: https://github.com/LegionIO/lex-interoception
|
|
51
|
+
licenses:
|
|
52
|
+
- MIT
|
|
53
|
+
metadata:
|
|
54
|
+
homepage_uri: https://github.com/LegionIO/lex-interoception
|
|
55
|
+
source_code_uri: https://github.com/LegionIO/lex-interoception
|
|
56
|
+
documentation_uri: https://github.com/LegionIO/lex-interoception
|
|
57
|
+
changelog_uri: https://github.com/LegionIO/lex-interoception
|
|
58
|
+
bug_tracker_uri: https://github.com/LegionIO/lex-interoception/issues
|
|
59
|
+
rubygems_mfa_required: 'true'
|
|
60
|
+
rdoc_options: []
|
|
61
|
+
require_paths:
|
|
62
|
+
- lib
|
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '3.4'
|
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '0'
|
|
73
|
+
requirements: []
|
|
74
|
+
rubygems_version: 3.6.9
|
|
75
|
+
specification_version: 4
|
|
76
|
+
summary: LEX Interoception
|
|
77
|
+
test_files: []
|