lex-agency 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: f8d9aa130b27351ac7ac2fe1e8e0e0e388adb4e57faa1ee9daa0da8470030868
4
+ data.tar.gz: 2a1535b7b18bcb4f5b6aff59cb1cc5c90d27a446e34cadfb649a1d55aa70ace5
5
+ SHA512:
6
+ metadata.gz: 293a35bd4e2d348417428ab8432d3ff95ace43d7ace37cdaecf8c82ea21717df37fa0657282ad31b8b9387b8e25998a3bea8fa7c5f980d3d5a724c89d506d3fe
7
+ data.tar.gz: 9de9f8f5c7c7b4ec070c06c7bee78e6700ebe7bdba66d688abcbbbf1b9940439bbe34a1261d5a37d1d23ea4681ede1d3d0eea5f818b4ee6765e12a18631e238c
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matthew Iverson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # lex-agency
2
+
3
+ Self-efficacy and agency modeling for LegionIO — implements Bandura's self-efficacy theory for agentic AI.
4
+
5
+ ## What It Does
6
+
7
+ Tracks the agent's belief in its own ability to achieve outcomes across different domains. Uses Bandura's four sources of self-efficacy: mastery experiences (direct outcomes), vicarious learning (observing others), verbal persuasion (being told you can/can't), and physiological states. Efficacy scores determine whether the agent should attempt tasks and are used to prioritize domain engagement.
8
+
9
+ ## Core Concept: Domain-Level Self-Efficacy
10
+
11
+ Each domain maintains an efficacy score (0.05–0.98) updated via EMA when outcomes are recorded. Failures hit harder than successes (asymmetric update):
12
+
13
+ ```ruby
14
+ # Direct mastery experience has the highest impact
15
+ client.record_mastery(domain: :terraform, outcome_type: :success, magnitude: 1.0)
16
+
17
+ # Learning from others has 0.4x the impact
18
+ client.record_vicarious(domain: :kubernetes, outcome_type: :success)
19
+
20
+ # Gate whether to attempt an action
21
+ result = client.should_attempt?(domain: :terraform, threshold: 0.3)
22
+ # => { should_attempt: true, efficacy: 0.72, label: :capable }
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```ruby
28
+ client = Legion::Extensions::Agency::Client.new
29
+
30
+ # Record outcomes from all four sources
31
+ client.record_mastery(domain: :networking, outcome_type: :failure, attribution: :full_agency)
32
+ client.record_vicarious(domain: :security, outcome_type: :success, magnitude: 0.8)
33
+ client.record_persuasion(domain: :ml, positive: true, magnitude: 0.6)
34
+ client.record_physiological(domain: :reasoning, state: :energized)
35
+
36
+ # Query efficacy
37
+ client.check_efficacy(domain: :networking)
38
+ # => { efficacy: 0.38, label: :doubtful, success_rate: 0.2, history_count: 3 }
39
+
40
+ # Find strongest and weakest domains
41
+ client.strongest_domains(count: 3)
42
+ client.weakest_domains(count: 3)
43
+
44
+ # Maintenance (decay unused domains toward default)
45
+ client.update_agency
46
+ ```
47
+
48
+ ## Integration
49
+
50
+ Wire `should_attempt?` into lex-tick's `action_selection` phase to prevent the agent from attempting tasks in domains where it has learned it is ineffective. Call `record_mastery` after task completion to build accurate per-domain confidence over time.
51
+
52
+ ## Development
53
+
54
+ ```bash
55
+ bundle install
56
+ bundle exec rspec
57
+ bundle exec rubocop
58
+ ```
59
+
60
+ ## License
61
+
62
+ MIT
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Agency
6
+ class Client
7
+ include Runners::Agency
8
+
9
+ attr_reader :efficacy_model
10
+
11
+ def initialize(efficacy_model: nil, **)
12
+ @efficacy_model = efficacy_model || Helpers::EfficacyModel.new
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Agency
6
+ module Helpers
7
+ module Constants
8
+ # Initial self-efficacy for new domains (moderate confidence)
9
+ DEFAULT_EFFICACY = 0.5
10
+
11
+ # EMA alpha for efficacy updates (slow adaptation)
12
+ EFFICACY_ALPHA = 0.12
13
+
14
+ # How much mastery success boosts efficacy
15
+ MASTERY_BOOST = 0.15
16
+
17
+ # How much failure reduces efficacy (asymmetric — failures hit harder)
18
+ FAILURE_PENALTY = 0.20
19
+
20
+ # Vicarious learning multiplier (learning from others' outcomes)
21
+ VICARIOUS_MULTIPLIER = 0.4
22
+
23
+ # Verbal persuasion multiplier (being told you can/can't do something)
24
+ PERSUASION_MULTIPLIER = 0.25
25
+
26
+ # Physiological state influence on efficacy
27
+ PHYSIOLOGICAL_MULTIPLIER = 0.15
28
+
29
+ # Minimum efficacy (never zero — always some belief in possibility)
30
+ EFFICACY_FLOOR = 0.05
31
+
32
+ # Maximum efficacy (never perfectly certain)
33
+ EFFICACY_CEILING = 0.98
34
+
35
+ # Domain decay rate per tick (unused domains slowly regress toward default)
36
+ DECAY_RATE = 0.002
37
+
38
+ # Maximum tracked domains
39
+ MAX_DOMAINS = 100
40
+
41
+ # Maximum outcome history per domain
42
+ MAX_HISTORY_PER_DOMAIN = 50
43
+
44
+ # Maximum total outcome events
45
+ MAX_TOTAL_HISTORY = 500
46
+
47
+ # Sources of efficacy information (Bandura's four sources)
48
+ EFFICACY_SOURCES = %i[mastery vicarious persuasion physiological].freeze
49
+
50
+ # Agency attribution levels
51
+ ATTRIBUTION_LEVELS = {
52
+ full_agency: 0.8,
53
+ partial_agency: 0.5,
54
+ low_agency: 0.3,
55
+ no_agency: 0.0
56
+ }.freeze
57
+
58
+ # Outcome types
59
+ OUTCOME_TYPES = %i[success failure partial_success unexpected].freeze
60
+
61
+ # Efficacy level labels
62
+ EFFICACY_LABELS = {
63
+ (0.8..) => :highly_capable,
64
+ (0.6...0.8) => :capable,
65
+ (0.4...0.6) => :uncertain,
66
+ (0.2...0.4) => :doubtful,
67
+ (..0.2) => :helpless
68
+ }.freeze
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Agency
6
+ module Helpers
7
+ class EfficacyModel
8
+ attr_reader :domains, :history
9
+
10
+ def initialize
11
+ @domains = {}
12
+ @history = []
13
+ end
14
+
15
+ def efficacy_for(domain)
16
+ @domains[domain] ||= Constants::DEFAULT_EFFICACY
17
+ @domains[domain]
18
+ end
19
+
20
+ def efficacy_label(domain)
21
+ value = efficacy_for(domain)
22
+ Constants::EFFICACY_LABELS.each do |range, label|
23
+ return label if range.cover?(value)
24
+ end
25
+ :uncertain
26
+ end
27
+
28
+ def record_outcome(event)
29
+ @history << event
30
+ update_efficacy(event)
31
+ trim_history
32
+ event
33
+ end
34
+
35
+ def decay_all
36
+ @domains.each_key do |domain|
37
+ current = @domains[domain]
38
+ diff = Constants::DEFAULT_EFFICACY - current
39
+ @domains[domain] = (current + (diff * Constants::DECAY_RATE)).clamp(
40
+ Constants::EFFICACY_FLOOR, Constants::EFFICACY_CEILING
41
+ )
42
+ end
43
+ trim_domains
44
+ end
45
+
46
+ def domain_history(domain)
47
+ @history.select { |e| e.domain == domain }
48
+ end
49
+
50
+ def success_rate(domain)
51
+ events = domain_history(domain)
52
+ return 0.0 if events.empty?
53
+
54
+ successes = events.count(&:success?)
55
+ successes.to_f / events.size
56
+ end
57
+
58
+ def strongest_domains(count = 5)
59
+ @domains.sort_by { |_, v| -v }.first(count).to_h
60
+ end
61
+
62
+ def weakest_domains(count = 5)
63
+ @domains.sort_by { |_, v| v }.first(count).to_h
64
+ end
65
+
66
+ def overall_efficacy
67
+ return Constants::DEFAULT_EFFICACY if @domains.empty?
68
+
69
+ @domains.values.sum / @domains.size
70
+ end
71
+
72
+ def domain_count
73
+ @domains.size
74
+ end
75
+
76
+ def to_h
77
+ {
78
+ domain_count: @domains.size,
79
+ overall_efficacy: overall_efficacy.round(4),
80
+ history_size: @history.size,
81
+ domains: @domains.transform_values { |v| v.round(4) }
82
+ }
83
+ end
84
+
85
+ private
86
+
87
+ def update_efficacy(event)
88
+ domain = event.domain
89
+ current = efficacy_for(domain)
90
+ delta = compute_delta(event)
91
+
92
+ new_value = current + (Constants::EFFICACY_ALPHA * delta)
93
+ @domains[domain] = new_value.clamp(Constants::EFFICACY_FLOOR, Constants::EFFICACY_CEILING)
94
+ end
95
+
96
+ def compute_delta(event)
97
+ base = event.attributed_magnitude
98
+ multiplier = source_multiplier(event.source)
99
+
100
+ if event.success?
101
+ base * multiplier * Constants::MASTERY_BOOST / Constants::EFFICACY_ALPHA
102
+ else
103
+ -base * multiplier * Constants::FAILURE_PENALTY / Constants::EFFICACY_ALPHA
104
+ end
105
+ end
106
+
107
+ def source_multiplier(source)
108
+ case source
109
+ when :mastery then 1.0
110
+ when :vicarious then Constants::VICARIOUS_MULTIPLIER
111
+ when :persuasion then Constants::PERSUASION_MULTIPLIER
112
+ when :physiological then Constants::PHYSIOLOGICAL_MULTIPLIER
113
+ else 0.5
114
+ end
115
+ end
116
+
117
+ def trim_history
118
+ @history.shift(@history.size - Constants::MAX_TOTAL_HISTORY) if @history.size > Constants::MAX_TOTAL_HISTORY
119
+ end
120
+
121
+ def trim_domains
122
+ return unless @domains.size > Constants::MAX_DOMAINS
123
+
124
+ sorted = @domains.sort_by { |_, v| (v - Constants::DEFAULT_EFFICACY).abs }
125
+ excess = @domains.size - Constants::MAX_DOMAINS
126
+ sorted.first(excess).each { |domain, _| @domains.delete(domain) } # rubocop:disable Style/HashEachMethods
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Agency
8
+ module Helpers
9
+ class OutcomeEvent
10
+ attr_reader :id, :domain, :outcome_type, :source, :magnitude, :attribution, :timestamp
11
+
12
+ def initialize(domain:, outcome_type:, source: :mastery, magnitude: 1.0, attribution: :full_agency)
13
+ @id = SecureRandom.uuid
14
+ @domain = domain
15
+ @outcome_type = outcome_type
16
+ @source = source
17
+ @magnitude = magnitude.clamp(0.0, 1.0)
18
+ @attribution = attribution
19
+ @timestamp = Time.now.utc
20
+ end
21
+
22
+ def success?
23
+ %i[success partial_success].include?(@outcome_type)
24
+ end
25
+
26
+ def attributed_magnitude
27
+ level = Constants::ATTRIBUTION_LEVELS[@attribution] || 0.5
28
+ @magnitude * level
29
+ end
30
+
31
+ def to_h
32
+ {
33
+ id: @id,
34
+ domain: @domain,
35
+ outcome_type: @outcome_type,
36
+ source: @source,
37
+ magnitude: @magnitude,
38
+ attribution: @attribution,
39
+ success: success?,
40
+ attributed_magnitude: attributed_magnitude.round(4),
41
+ timestamp: @timestamp
42
+ }
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Agency
6
+ module Runners
7
+ module Agency
8
+ include Legion::Extensions::Helpers::Lex
9
+
10
+ def efficacy_model
11
+ @efficacy_model ||= Helpers::EfficacyModel.new
12
+ end
13
+
14
+ def record_mastery(domain:, outcome_type:, magnitude: 1.0, attribution: :full_agency, **)
15
+ event = Helpers::OutcomeEvent.new(
16
+ domain: domain, outcome_type: outcome_type, source: :mastery,
17
+ magnitude: magnitude, attribution: attribution
18
+ )
19
+ efficacy_model.record_outcome(event)
20
+ Legion::Logging.debug "[agency] mastery #{outcome_type} in #{domain} " \
21
+ "efficacy=#{efficacy_model.efficacy_for(domain).round(4)}"
22
+ { success: true, event: event.to_h, efficacy: efficacy_model.efficacy_for(domain).round(4) }
23
+ end
24
+
25
+ def record_vicarious(domain:, outcome_type:, magnitude: 1.0, **)
26
+ event = Helpers::OutcomeEvent.new(
27
+ domain: domain, outcome_type: outcome_type, source: :vicarious,
28
+ magnitude: magnitude, attribution: :partial_agency
29
+ )
30
+ efficacy_model.record_outcome(event)
31
+ Legion::Logging.debug "[agency] vicarious #{outcome_type} in #{domain}"
32
+ { success: true, event: event.to_h, efficacy: efficacy_model.efficacy_for(domain).round(4) }
33
+ end
34
+
35
+ def record_persuasion(domain:, positive: true, magnitude: 0.5, **)
36
+ outcome = positive ? :success : :failure
37
+ event = Helpers::OutcomeEvent.new(
38
+ domain: domain, outcome_type: outcome, source: :persuasion,
39
+ magnitude: magnitude, attribution: :partial_agency
40
+ )
41
+ efficacy_model.record_outcome(event)
42
+ Legion::Logging.debug "[agency] persuasion #{positive ? 'positive' : 'negative'} in #{domain}"
43
+ { success: true, event: event.to_h, efficacy: efficacy_model.efficacy_for(domain).round(4) }
44
+ end
45
+
46
+ def record_physiological(domain:, state: :energized, **)
47
+ outcome = %i[energized calm focused].include?(state) ? :success : :failure
48
+ magnitude = %i[energized calm focused].include?(state) ? 0.6 : 0.4
49
+ event = Helpers::OutcomeEvent.new(
50
+ domain: domain, outcome_type: outcome, source: :physiological,
51
+ magnitude: magnitude, attribution: :low_agency
52
+ )
53
+ efficacy_model.record_outcome(event)
54
+ Legion::Logging.debug "[agency] physiological #{state} in #{domain}"
55
+ { success: true, event: event.to_h, efficacy: efficacy_model.efficacy_for(domain).round(4) }
56
+ end
57
+
58
+ def update_agency(**)
59
+ efficacy_model.decay_all
60
+ Legion::Logging.debug "[agency] tick: domains=#{efficacy_model.domain_count} " \
61
+ "overall=#{efficacy_model.overall_efficacy.round(4)}"
62
+ { success: true, stats: efficacy_model.to_h }
63
+ end
64
+
65
+ def check_efficacy(domain:, **)
66
+ {
67
+ success: true,
68
+ domain: domain,
69
+ efficacy: efficacy_model.efficacy_for(domain).round(4),
70
+ label: efficacy_model.efficacy_label(domain),
71
+ success_rate: efficacy_model.success_rate(domain).round(4),
72
+ history_count: efficacy_model.domain_history(domain).size
73
+ }
74
+ end
75
+
76
+ def should_attempt?(domain:, threshold: 0.3, **)
77
+ efficacy = efficacy_model.efficacy_for(domain)
78
+ {
79
+ success: true,
80
+ domain: domain,
81
+ efficacy: efficacy.round(4),
82
+ threshold: threshold,
83
+ should_attempt: efficacy >= threshold,
84
+ label: efficacy_model.efficacy_label(domain)
85
+ }
86
+ end
87
+
88
+ def strongest_domains(count: 5, **)
89
+ domains = efficacy_model.strongest_domains(count)
90
+ {
91
+ success: true,
92
+ domains: domains.transform_values { |v| v.round(4) },
93
+ count: domains.size
94
+ }
95
+ end
96
+
97
+ def weakest_domains(count: 5, **)
98
+ domains = efficacy_model.weakest_domains(count)
99
+ {
100
+ success: true,
101
+ domains: domains.transform_values { |v| v.round(4) },
102
+ count: domains.size
103
+ }
104
+ end
105
+
106
+ def agency_stats(**)
107
+ { success: true, stats: efficacy_model.to_h }
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Agency
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'agency/version'
4
+ require_relative 'agency/helpers/constants'
5
+ require_relative 'agency/helpers/outcome_event'
6
+ require_relative 'agency/helpers/efficacy_model'
7
+ require_relative 'agency/runners/agency'
8
+ require_relative 'agency/client'
9
+
10
+ module Legion
11
+ module Extensions
12
+ module Agency
13
+ extend Legion::Extensions::Core if defined?(Legion::Extensions::Core)
14
+ end
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-agency
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: Implements Bandura's self-efficacy theory for LegionIO agents — tracks
27
+ belief in ability to achieve outcomes across domains, mastery experiences, vicarious
28
+ learning, and agency attribution.
29
+ email:
30
+ - matt@iverson.io
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - LICENSE
36
+ - README.md
37
+ - lib/legion/extensions/agency.rb
38
+ - lib/legion/extensions/agency/client.rb
39
+ - lib/legion/extensions/agency/helpers/constants.rb
40
+ - lib/legion/extensions/agency/helpers/efficacy_model.rb
41
+ - lib/legion/extensions/agency/helpers/outcome_event.rb
42
+ - lib/legion/extensions/agency/runners/agency.rb
43
+ - lib/legion/extensions/agency/version.rb
44
+ homepage: https://github.com/LegionIO/lex-agency
45
+ licenses:
46
+ - MIT
47
+ metadata:
48
+ rubygems_mfa_required: 'true'
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '3.4'
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubygems_version: 3.6.9
64
+ specification_version: 4
65
+ summary: Self-efficacy and agency modeling for LegionIO
66
+ test_files: []