lex-motivation 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6cf669a3b751e11795fd50424fec3647cdeee856fbb181667d5125c9d694db98
4
+ data.tar.gz: 2f3147e4f59ed722387020c994423e39d66a6f0764a2eca6f106425c04a9ee81
5
+ SHA512:
6
+ metadata.gz: 5f965e9421b730abcdaa5ed951f5804a7fae77df615ef6c0c138316d16207fc0b37c0a47f48a8060a9e1fcf4b379590edc35e8fe957de00c9d0129349a103482
7
+ data.tar.gz: 9d51bae33770415f66acda673e39235d4828acd302b0c860b986cdeb6c266c72cc8b07332744b55158fa2b5b341138d82e1b40cea711e39df7a8c6e31d20515a
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/motivation/helpers/constants'
4
+ require 'legion/extensions/motivation/helpers/drive_state'
5
+ require 'legion/extensions/motivation/helpers/motivation_store'
6
+ require 'legion/extensions/motivation/runners/motivation'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module Motivation
11
+ class Client
12
+ include Runners::Motivation
13
+
14
+ attr_reader :motivation_store
15
+
16
+ def initialize(motivation_store: nil, **)
17
+ @motivation_store = motivation_store || Helpers::MotivationStore.new
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Motivation
6
+ module Helpers
7
+ module Constants
8
+ # Drive types based on Self-Determination Theory + survival/obligation
9
+ DRIVE_TYPES = %i[autonomy competence relatedness novelty obligation survival].freeze
10
+
11
+ # Motivation operating modes (approach/avoidance model)
12
+ MOTIVATION_MODES = %i[approach avoidance maintenance dormant].freeze
13
+
14
+ # EMA alpha for drive level tracking (slow adaptation)
15
+ DRIVE_ALPHA = 0.1
16
+
17
+ # Intrinsic drives (Self-Determination Theory: autonomy, competence, relatedness + novelty)
18
+ INTRINSIC_DRIVES = %i[autonomy competence relatedness novelty].freeze
19
+
20
+ # Extrinsic drives (external pressure: obligation, survival)
21
+ EXTRINSIC_DRIVES = %i[obligation survival].freeze
22
+
23
+ # Per-tick drive decay rate
24
+ DRIVE_DECAY_RATE = 0.02
25
+
26
+ # Maximum goals tracked simultaneously
27
+ MAX_GOALS = 50
28
+
29
+ # Above this overall level, agent is in approach mode
30
+ APPROACH_THRESHOLD = 0.6
31
+
32
+ # Below this, agent is in avoidance mode
33
+ AVOIDANCE_THRESHOLD = 0.3
34
+
35
+ # Below this across individual drives, drive is in burnout
36
+ BURNOUT_THRESHOLD = 0.15
37
+
38
+ # Below this across ALL drives, agent is amotivated
39
+ AMOTIVATION_THRESHOLD = 0.2
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Motivation
6
+ module Helpers
7
+ class DriveState
8
+ attr_reader :drives
9
+
10
+ def initialize
11
+ @drives = Constants::DRIVE_TYPES.to_h do |type|
12
+ [type, { level: 0.5, satisfied: false, last_signal: nil }]
13
+ end
14
+ end
15
+
16
+ def update_drive(type, signal)
17
+ return unless Constants::DRIVE_TYPES.include?(type)
18
+
19
+ clamped = signal.clamp(0.0, 1.0)
20
+ current = @drives[type][:level]
21
+ @drives[type][:level] = ema(current, clamped, Constants::DRIVE_ALPHA)
22
+ @drives[type][:satisfied] = @drives[type][:level] >= 0.6
23
+ @drives[type][:last_signal] = Time.now.utc
24
+ end
25
+
26
+ def drive_level(type)
27
+ return 0.0 unless Constants::DRIVE_TYPES.include?(type)
28
+
29
+ @drives[type][:level]
30
+ end
31
+
32
+ def satisfied?(type)
33
+ return false unless Constants::DRIVE_TYPES.include?(type)
34
+
35
+ @drives[type][:satisfied]
36
+ end
37
+
38
+ def intrinsic_average
39
+ levels = Constants::INTRINSIC_DRIVES.map { |d| @drives[d][:level] }
40
+ mean(levels)
41
+ end
42
+
43
+ def extrinsic_average
44
+ levels = Constants::EXTRINSIC_DRIVES.map { |d| @drives[d][:level] }
45
+ mean(levels)
46
+ end
47
+
48
+ def overall_level
49
+ all_levels = @drives.values.map { |d| d[:level] }
50
+ mean(all_levels)
51
+ end
52
+
53
+ def current_mode
54
+ level = overall_level
55
+ if level >= Constants::APPROACH_THRESHOLD
56
+ :approach
57
+ elsif level <= Constants::BURNOUT_THRESHOLD
58
+ :dormant
59
+ elsif level <= Constants::AVOIDANCE_THRESHOLD
60
+ :avoidance
61
+ else
62
+ :maintenance
63
+ end
64
+ end
65
+
66
+ def decay_all
67
+ @drives.each_key do |type|
68
+ current = @drives[type][:level]
69
+ decayed = [current - Constants::DRIVE_DECAY_RATE, 0.0].max
70
+ @drives[type][:level] = decayed
71
+ @drives[type][:satisfied] = decayed >= 0.6
72
+ end
73
+ end
74
+
75
+ def amotivated?
76
+ @drives.values.all? { |d| d[:level] < Constants::AMOTIVATION_THRESHOLD }
77
+ end
78
+
79
+ private
80
+
81
+ def ema(current, observed, alpha)
82
+ (current * (1.0 - alpha)) + (observed * alpha)
83
+ end
84
+
85
+ def mean(values)
86
+ return 0.0 if values.empty?
87
+
88
+ values.sum / values.size.to_f
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Motivation
6
+ module Helpers
7
+ class MotivationStore
8
+ attr_reader :drive_state, :goal_motivations
9
+
10
+ def initialize(drive_state: nil)
11
+ @drive_state = drive_state || DriveState.new
12
+ @goal_motivations = {}
13
+ @low_motivation_ticks = 0
14
+ end
15
+
16
+ def commit_goal(goal_id, drives)
17
+ valid_drives = Array(drives).select { |d| Constants::DRIVE_TYPES.include?(d) }
18
+ return false if valid_drives.empty?
19
+
20
+ trim_goals
21
+ @goal_motivations[goal_id] = {
22
+ drives: valid_drives,
23
+ energy: goal_energy_for(valid_drives),
24
+ committed: true,
25
+ committed_at: Time.now.utc
26
+ }
27
+ true
28
+ end
29
+
30
+ def release_goal(goal_id)
31
+ @goal_motivations.delete(goal_id)
32
+ true
33
+ end
34
+
35
+ def goal_energy(goal_id)
36
+ entry = @goal_motivations[goal_id]
37
+ return 0.0 unless entry
38
+
39
+ entry[:energy] = goal_energy_for(entry[:drives])
40
+ entry[:energy]
41
+ end
42
+
43
+ def most_motivated_goal
44
+ return nil if @goal_motivations.empty?
45
+
46
+ refreshed = @goal_motivations.transform_values do |entry|
47
+ entry.merge(energy: goal_energy_for(entry[:drives]))
48
+ end
49
+
50
+ best_id, best_entry = refreshed.max_by { |_, v| v[:energy] }
51
+ return nil unless best_id
52
+
53
+ { goal_id: best_id, energy: best_entry[:energy].round(4), drives: best_entry[:drives] }
54
+ end
55
+
56
+ def burnout_check
57
+ overall = @drive_state.overall_level
58
+ if overall < Constants::BURNOUT_THRESHOLD
59
+ @low_motivation_ticks += 1
60
+ else
61
+ @low_motivation_ticks = 0
62
+ end
63
+
64
+ {
65
+ burnout: @low_motivation_ticks >= 10,
66
+ low_motivation_ticks: @low_motivation_ticks,
67
+ overall_level: overall.round(4)
68
+ }
69
+ end
70
+
71
+ def stats
72
+ {
73
+ overall_level: @drive_state.overall_level.round(4),
74
+ current_mode: @drive_state.current_mode,
75
+ intrinsic_average: @drive_state.intrinsic_average.round(4),
76
+ extrinsic_average: @drive_state.extrinsic_average.round(4),
77
+ amotivated: @drive_state.amotivated?,
78
+ goal_count: @goal_motivations.size,
79
+ low_motivation_ticks: @low_motivation_ticks
80
+ }
81
+ end
82
+
83
+ private
84
+
85
+ def goal_energy_for(drives)
86
+ return 0.0 if drives.empty?
87
+
88
+ total = drives.sum { |d| @drive_state.drive_level(d) }
89
+ (total / drives.size.to_f).clamp(0.0, 1.0)
90
+ end
91
+
92
+ def trim_goals
93
+ return unless @goal_motivations.size >= Constants::MAX_GOALS
94
+
95
+ oldest_key = @goal_motivations.min_by { |_, v| v[:committed_at] }&.first
96
+ @goal_motivations.delete(oldest_key) if oldest_key
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Motivation
6
+ module Runners
7
+ module Motivation
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def update_motivation(tick_results: {}, **)
12
+ extract_drive_signals(tick_results)
13
+ motivation_store.drive_state.decay_all
14
+ burnout = motivation_store.burnout_check
15
+ mode = motivation_store.drive_state.current_mode
16
+
17
+ Legion::Logging.debug "[motivation] mode=#{mode} " \
18
+ "overall=#{motivation_store.drive_state.overall_level.round(3)} " \
19
+ "amotivated=#{motivation_store.drive_state.amotivated?}"
20
+
21
+ {
22
+ mode: mode,
23
+ overall_level: motivation_store.drive_state.overall_level.round(4),
24
+ intrinsic_average: motivation_store.drive_state.intrinsic_average.round(4),
25
+ extrinsic_average: motivation_store.drive_state.extrinsic_average.round(4),
26
+ amotivated: motivation_store.drive_state.amotivated?,
27
+ burnout: burnout[:burnout]
28
+ }
29
+ end
30
+
31
+ def signal_drive(drive:, signal:, **)
32
+ drive_sym = drive.to_sym
33
+ return { success: false, error: "unknown drive: #{drive}" } unless Helpers::Constants::DRIVE_TYPES.include?(drive_sym)
34
+
35
+ motivation_store.drive_state.update_drive(drive_sym, signal.to_f)
36
+ level = motivation_store.drive_state.drive_level(drive_sym)
37
+
38
+ Legion::Logging.debug "[motivation] drive signal: #{drive_sym}=#{level.round(3)}"
39
+ { success: true, drive: drive_sym, level: level.round(4) }
40
+ end
41
+
42
+ def commit_to_goal(goal_id:, drives:, **)
43
+ drive_syms = Array(drives).map(&:to_sym)
44
+ result = motivation_store.commit_goal(goal_id, drive_syms)
45
+
46
+ if result
47
+ energy = motivation_store.goal_energy(goal_id)
48
+ Legion::Logging.info "[motivation] committed goal=#{goal_id} energy=#{energy.round(3)}"
49
+ { success: true, goal_id: goal_id, energy: energy.round(4) }
50
+ else
51
+ Legion::Logging.warn "[motivation] commit_goal rejected: no valid drives for #{goal_id}"
52
+ { success: false, error: 'no valid drives provided' }
53
+ end
54
+ end
55
+
56
+ def release_goal(goal_id:, **)
57
+ motivation_store.release_goal(goal_id)
58
+ Legion::Logging.debug "[motivation] released goal=#{goal_id}"
59
+ { success: true, goal_id: goal_id }
60
+ end
61
+
62
+ def motivation_for(goal_id:, **)
63
+ energy = motivation_store.goal_energy(goal_id)
64
+ Legion::Logging.debug "[motivation] motivation_for goal=#{goal_id} energy=#{energy.round(3)}"
65
+ { goal_id: goal_id, energy: energy.round(4) }
66
+ end
67
+
68
+ def most_motivated_goal(**)
69
+ result = motivation_store.most_motivated_goal
70
+ Legion::Logging.debug "[motivation] most_motivated_goal=#{result&.fetch(:goal_id, nil)}"
71
+ result || { goal_id: nil, energy: 0.0, drives: [] }
72
+ end
73
+
74
+ def drive_status(**)
75
+ drives = motivation_store.drive_state.drives.transform_values do |d|
76
+ { level: d[:level].round(4), satisfied: d[:satisfied] }
77
+ end
78
+
79
+ Legion::Logging.debug '[motivation] drive_status'
80
+ {
81
+ drives: drives,
82
+ mode: motivation_store.drive_state.current_mode,
83
+ overall: motivation_store.drive_state.overall_level.round(4)
84
+ }
85
+ end
86
+
87
+ def motivation_stats(**)
88
+ Legion::Logging.debug '[motivation] stats'
89
+ motivation_store.stats
90
+ end
91
+
92
+ private
93
+
94
+ def motivation_store
95
+ @motivation_store ||= Helpers::MotivationStore.new
96
+ end
97
+
98
+ def extract_drive_signals(tick_results)
99
+ extract_autonomy_signal(tick_results)
100
+ extract_competence_signal(tick_results)
101
+ extract_relatedness_signal(tick_results)
102
+ extract_novelty_signal(tick_results)
103
+ extract_obligation_signal(tick_results)
104
+ extract_survival_signal(tick_results)
105
+ end
106
+
107
+ def extract_autonomy_signal(tick_results)
108
+ consent_tier = tick_results.dig(:consent, :tier)
109
+ return unless consent_tier
110
+
111
+ signal = case consent_tier
112
+ when :autonomous then 1.0
113
+ when :collaborate then 0.7
114
+ when :consult then 0.4
115
+ else 0.1
116
+ end
117
+ motivation_store.drive_state.update_drive(:autonomy, signal)
118
+ end
119
+
120
+ def extract_competence_signal(tick_results)
121
+ accuracy = tick_results.dig(:prediction_engine, :accuracy)
122
+ return unless accuracy
123
+
124
+ motivation_store.drive_state.update_drive(:competence, accuracy.to_f)
125
+ end
126
+
127
+ def extract_relatedness_signal(tick_results)
128
+ trust_level = tick_results.dig(:trust, :overall_level)
129
+ return unless trust_level
130
+
131
+ motivation_store.drive_state.update_drive(:relatedness, trust_level.to_f)
132
+ end
133
+
134
+ def extract_novelty_signal(tick_results)
135
+ novel = tick_results.dig(:memory_retrieval, :novel_traces)
136
+ return unless novel
137
+
138
+ signal = novel.positive? ? [novel.to_f / 10.0, 1.0].min : 0.1
139
+ motivation_store.drive_state.update_drive(:novelty, signal)
140
+ end
141
+
142
+ def extract_obligation_signal(tick_results)
143
+ pending = tick_results.dig(:scheduler, :pending_tasks)
144
+ return unless pending
145
+
146
+ signal = pending.positive? ? [pending.to_f / 20.0, 1.0].min : 0.0
147
+ motivation_store.drive_state.update_drive(:obligation, signal)
148
+ end
149
+
150
+ def extract_survival_signal(tick_results)
151
+ extinction_level = tick_results.dig(:extinction, :level)
152
+ return unless extinction_level
153
+
154
+ signal = extinction_level.to_f / 4.0
155
+ motivation_store.drive_state.update_drive(:survival, signal)
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Motivation
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/motivation/version'
4
+ require 'legion/extensions/motivation/helpers/constants'
5
+ require 'legion/extensions/motivation/helpers/drive_state'
6
+ require 'legion/extensions/motivation/helpers/motivation_store'
7
+ require 'legion/extensions/motivation/runners/motivation'
8
+ require 'legion/extensions/motivation/client'
9
+
10
+ module Legion
11
+ module Extensions
12
+ module Motivation
13
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined?(:Core)
14
+ end
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-motivation
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matthew Iverson
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: legion-gaia
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ description: Models motivational drive states — why the agent acts, what energizes
27
+ behavior, and how approach vs avoidance tendencies operate
28
+ email:
29
+ - matt@legionIO.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - lib/legion/extensions/motivation.rb
35
+ - lib/legion/extensions/motivation/client.rb
36
+ - lib/legion/extensions/motivation/helpers/constants.rb
37
+ - lib/legion/extensions/motivation/helpers/drive_state.rb
38
+ - lib/legion/extensions/motivation/helpers/motivation_store.rb
39
+ - lib/legion/extensions/motivation/runners/motivation.rb
40
+ - lib/legion/extensions/motivation/version.rb
41
+ homepage: https://github.com/LegionIO/lex-motivation
42
+ licenses:
43
+ - MIT
44
+ metadata:
45
+ rubygems_mfa_required: 'true'
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '3.4'
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubygems_version: 3.6.9
61
+ specification_version: 4
62
+ summary: Goal-directed motivation and drive states for LegionIO cognitive agents
63
+ test_files: []