lex-predictive-coding 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: be107566fcfb814bdea9f4857e1023b4cbf47ca1d2959126f28e518bf909b7f3
4
+ data.tar.gz: 9ce453e5312273b215249a6b2bf77456dfec0e2ec252e48bf60bf0e7377d34db
5
+ SHA512:
6
+ metadata.gz: 2f79dd0c0450927598f12c3f16e15001feeadf8f5582817420634579259d40d6be233491dff9d3546a3435f1ad5d0236b55ec44bff64ac313c3d936031cac84a
7
+ data.tar.gz: d5c4493f99c551d3ce1bcd9062f8cc78e62bf775ef9ad61581be8acc405d6d8845a9b3cf2e751afedd6cbca0292a627800661dfea9b68854346bb3cd4e2fb1b4
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ gem 'rspec', '~> 3.13'
8
+ gem 'rubocop', '~> 1.75', require: false
9
+ gem 'rubocop-rspec', require: false
10
+
11
+ gem 'legion-gaia', path: '../../legion-gaia'
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/predictive_coding/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-predictive-coding'
7
+ spec.version = Legion::Extensions::PredictiveCoding::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Predictive Coding'
12
+ spec.description = "Karl Friston's Free Energy Principle / Predictive Processing framework for brain-modeled agentic AI"
13
+ spec.homepage = 'https://github.com/LegionIO/lex-predictive-coding'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = '>= 3.4'
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-predictive-coding'
19
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-predictive-coding'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-predictive-coding'
21
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-predictive-coding/issues'
22
+ spec.metadata['rubygems_mfa_required'] = 'true'
23
+
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ Dir.glob('{lib,spec}/**/*') + %w[lex-predictive-coding.gemspec Gemfile]
26
+ end
27
+ spec.require_paths = ['lib']
28
+ spec.add_development_dependency 'legion-gaia'
29
+ 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 PredictiveCoding
8
+ module Actor
9
+ class Decay < Legion::Extensions::Actors::Every
10
+ def runner_class
11
+ Legion::Extensions::PredictiveCoding::Runners::PredictiveCoding
12
+ end
13
+
14
+ def runner_function
15
+ 'update_predictive_coding'
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/predictive_coding/helpers/constants'
4
+ require 'legion/extensions/predictive_coding/helpers/prediction_error'
5
+ require 'legion/extensions/predictive_coding/helpers/generative_model'
6
+ require 'legion/extensions/predictive_coding/runners/predictive_coding'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module PredictiveCoding
11
+ class Client
12
+ include Runners::PredictiveCoding
13
+
14
+ def initialize(generative_model: nil, **)
15
+ @generative_model = generative_model || Helpers::GenerativeModel.new
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :generative_model
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module PredictiveCoding
6
+ module Helpers
7
+ module Constants
8
+ MAX_PREDICTIONS = 200
9
+ MAX_ERROR_HISTORY = 500
10
+ MAX_MODELS = 20
11
+ DEFAULT_PRECISION = 0.5
12
+ PRECISION_FLOOR = 0.05
13
+ PRECISION_ALPHA = 0.12 # EMA for precision updates
14
+ ERROR_ALPHA = 0.15 # EMA for prediction error smoothing
15
+ MODEL_LEARNING_RATE = 0.1
16
+ FREE_ENERGY_ALPHA = 0.1 # EMA for free energy tracking
17
+ COMPLEXITY_PENALTY = 0.05 # penalizes overly complex models
18
+ PREDICTION_DECAY = 0.01
19
+ PRECISION_DECAY = 0.005
20
+ MAX_ACTIVE_INFERENCES = 50
21
+ SURPRISE_THRESHOLD = 0.7 # above this, prediction error is "surprising"
22
+
23
+ PREDICTION_ERROR_LEVELS = {
24
+ negligible: 0.0..0.1,
25
+ low: 0.1..0.3,
26
+ moderate: 0.3..0.5,
27
+ high: 0.5..0.7,
28
+ surprising: 0.7..1.0
29
+ }.freeze
30
+
31
+ FREE_ENERGY_LEVELS = {
32
+ minimal: 0.0..0.2,
33
+ low: 0.2..0.4,
34
+ moderate: 0.4..0.6,
35
+ elevated: 0.6..0.8,
36
+ critical: 0.8..Float::INFINITY
37
+ }.freeze
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module PredictiveCoding
6
+ module Helpers
7
+ class GenerativeModel
8
+ attr_reader :model_id, :created_at
9
+
10
+ def initialize(model_id: nil)
11
+ @model_id = model_id || SecureRandom.uuid
12
+ @created_at = Time.now.utc
13
+ @predictions = {} # domain -> { value, confidence, updated_at }
14
+ @error_history = [] # array of PredictionError objects (capped)
15
+ @precisions = {} # domain -> float (0..1)
16
+ @free_energy_ema = 0.0
17
+ @domain_models = {} # domain -> simple weighted mean tracker
18
+ end
19
+
20
+ def predict(domain:, context: {})
21
+ prior = @domain_models[domain]
22
+ if prior
23
+ confidence = @precisions.fetch(domain, Constants::DEFAULT_PRECISION)
24
+ value = prior[:mean]
25
+ else
26
+ confidence = Constants::DEFAULT_PRECISION
27
+ value = context[:expected] || context[:baseline] || 0.5
28
+ end
29
+
30
+ @predictions[domain] = { value: value, confidence: confidence, updated_at: Time.now.utc }
31
+
32
+ { domain: domain, predicted: value, confidence: confidence }
33
+ end
34
+
35
+ def update(domain:, predicted:, actual:)
36
+ precision = @precisions.fetch(domain, Constants::DEFAULT_PRECISION)
37
+ error = PredictionError.new(domain: domain, predicted: predicted, actual: actual, precision: precision)
38
+
39
+ record_error(error)
40
+ update_precision(domain, error.error_magnitude)
41
+ update_domain_model(domain, actual)
42
+ update_free_energy(error.error_magnitude)
43
+
44
+ error
45
+ end
46
+
47
+ def precision_for(domain:)
48
+ @precisions.fetch(domain, Constants::DEFAULT_PRECISION)
49
+ end
50
+
51
+ def free_energy
52
+ prediction_error_term = average_weighted_error
53
+ complexity_term = @domain_models.size * Constants::COMPLEXITY_PENALTY
54
+ @free_energy_ema = ema(@free_energy_ema, prediction_error_term + complexity_term, Constants::FREE_ENERGY_ALPHA)
55
+ @free_energy_ema
56
+ end
57
+
58
+ def free_energy_level
59
+ fe = free_energy
60
+ Constants::FREE_ENERGY_LEVELS.find { |_k, range| range.cover?(fe) }&.first || :unknown
61
+ end
62
+
63
+ def active_inference_candidates
64
+ @domain_models.keys.select do |domain|
65
+ precision = @precisions.fetch(domain, Constants::DEFAULT_PRECISION)
66
+ recent_errors = recent_errors_for(domain)
67
+ next false if recent_errors.empty?
68
+
69
+ avg_error = recent_errors.sum(&:error_magnitude) / recent_errors.size
70
+ avg_error > Constants::SURPRISE_THRESHOLD && precision < 0.6
71
+ end
72
+ end
73
+
74
+ def surprising_errors
75
+ @error_history.select(&:surprising?)
76
+ end
77
+
78
+ def all_errors
79
+ @error_history
80
+ end
81
+
82
+ def decay_all
83
+ @precisions.each_key do |domain|
84
+ current = @precisions[domain]
85
+ decayed = [current - Constants::PRECISION_DECAY, Constants::PRECISION_FLOOR].max
86
+ @precisions[domain] = decayed
87
+ end
88
+
89
+ prune_old_errors
90
+ end
91
+
92
+ def domain_count
93
+ @domain_models.size
94
+ end
95
+
96
+ def error_count
97
+ @error_history.size
98
+ end
99
+
100
+ def to_h
101
+ {
102
+ model_id: @model_id,
103
+ created_at: @created_at,
104
+ domain_count: @domain_models.size,
105
+ error_count: @error_history.size,
106
+ free_energy: free_energy.round(4),
107
+ free_energy_level: free_energy_level,
108
+ surprising_count: surprising_errors.size,
109
+ domains: domain_stats
110
+ }
111
+ end
112
+
113
+ private
114
+
115
+ def record_error(error)
116
+ @error_history << error
117
+ @error_history.shift while @error_history.size > Constants::MAX_ERROR_HISTORY
118
+ end
119
+
120
+ def update_precision(domain, error_magnitude)
121
+ current = @precisions.fetch(domain, Constants::DEFAULT_PRECISION)
122
+ # High error -> precision decreases; low error -> precision increases
123
+ signal = 1.0 - error_magnitude
124
+ updated = ema(current, signal, Constants::PRECISION_ALPHA)
125
+ @precisions[domain] = [updated, Constants::PRECISION_FLOOR].max
126
+ end
127
+
128
+ def update_domain_model(domain, actual)
129
+ if @domain_models[domain]
130
+ model = @domain_models[domain]
131
+ model[:count] += 1
132
+ model[:mean] = ema(model[:mean], actual.to_f, Constants::MODEL_LEARNING_RATE)
133
+ else
134
+ @domain_models[domain] = { mean: actual.to_f, count: 1 }
135
+ end
136
+
137
+ return unless @domain_models.size > Constants::MAX_MODELS
138
+
139
+ oldest_domain = @domain_models.keys.first
140
+ @domain_models.delete(oldest_domain)
141
+ @precisions.delete(oldest_domain)
142
+ end
143
+
144
+ def update_free_energy(error_magnitude)
145
+ complexity = @domain_models.size * Constants::COMPLEXITY_PENALTY
146
+ raw = error_magnitude + complexity
147
+ @free_energy_ema = ema(@free_energy_ema, raw, Constants::FREE_ENERGY_ALPHA)
148
+ end
149
+
150
+ def average_weighted_error
151
+ return 0.0 if @error_history.empty?
152
+
153
+ recent = @error_history.last(50)
154
+ recent.sum(&:weighted_error) / recent.size
155
+ end
156
+
157
+ def recent_errors_for(domain)
158
+ @error_history.select { |e| e.domain == domain }.last(10)
159
+ end
160
+
161
+ def prune_old_errors
162
+ @error_history.shift while @error_history.size > Constants::MAX_ERROR_HISTORY
163
+ end
164
+
165
+ def ema(current, new_value, alpha)
166
+ (alpha * new_value) + ((1.0 - alpha) * current)
167
+ end
168
+
169
+ def domain_stats
170
+ @domain_models.map do |domain, model|
171
+ {
172
+ domain: domain,
173
+ mean: model[:mean].round(4),
174
+ count: model[:count],
175
+ precision: @precisions.fetch(domain, Constants::DEFAULT_PRECISION).round(4)
176
+ }
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module PredictiveCoding
6
+ module Helpers
7
+ class PredictionError
8
+ attr_reader :domain, :predicted, :actual, :error_magnitude, :precision, :weighted_error, :timestamp
9
+
10
+ def initialize(domain:, predicted:, actual:, precision: Constants::DEFAULT_PRECISION)
11
+ @domain = domain
12
+ @predicted = predicted
13
+ @actual = actual
14
+ @error_magnitude = compute_error_magnitude(predicted, actual)
15
+ @precision = precision
16
+ @weighted_error = @error_magnitude * @precision
17
+ @timestamp = Time.now.utc
18
+ end
19
+
20
+ def surprising?
21
+ @error_magnitude >= Constants::SURPRISE_THRESHOLD
22
+ end
23
+
24
+ def level
25
+ Constants::PREDICTION_ERROR_LEVELS.find { |_k, range| range.cover?(@error_magnitude) }&.first || :unknown
26
+ end
27
+
28
+ def to_h
29
+ {
30
+ domain: @domain,
31
+ predicted: @predicted,
32
+ actual: @actual,
33
+ error_magnitude: @error_magnitude,
34
+ precision: @precision,
35
+ weighted_error: @weighted_error,
36
+ surprising: surprising?,
37
+ level: level,
38
+ timestamp: @timestamp
39
+ }
40
+ end
41
+
42
+ private
43
+
44
+ def compute_error_magnitude(predicted, actual)
45
+ if predicted.is_a?(Numeric) && actual.is_a?(Numeric)
46
+ (predicted - actual).abs.clamp(0.0, 1.0)
47
+ else
48
+ predicted == actual ? 0.0 : 1.0
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module PredictiveCoding
8
+ module Runners
9
+ module PredictiveCoding
10
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
11
+ Legion::Extensions::Helpers.const_defined?(:Lex)
12
+
13
+ def generate_prediction(domain:, context: {}, **)
14
+ prediction = generative_model.predict(domain: domain, context: context)
15
+ Legion::Logging.debug "[predictive_coding] generate_prediction domain=#{domain} " \
16
+ "predicted=#{prediction[:predicted]} confidence=#{prediction[:confidence].round(3)}"
17
+ { success: true, domain: domain, predicted: prediction[:predicted], confidence: prediction[:confidence] }
18
+ end
19
+
20
+ def report_outcome(domain:, predicted:, actual:, **)
21
+ error = generative_model.update(domain: domain, predicted: predicted, actual: actual)
22
+ Legion::Logging.debug "[predictive_coding] report_outcome domain=#{domain} " \
23
+ "error_magnitude=#{error.error_magnitude.round(3)} surprising=#{error.surprising?}"
24
+ {
25
+ success: true,
26
+ domain: domain,
27
+ error_magnitude: error.error_magnitude,
28
+ weighted_error: error.weighted_error,
29
+ precision: error.precision,
30
+ surprising: error.surprising?,
31
+ level: error.level
32
+ }
33
+ end
34
+
35
+ def precision_for(domain:, **)
36
+ value = generative_model.precision_for(domain: domain)
37
+ Legion::Logging.debug "[predictive_coding] precision_for domain=#{domain} precision=#{value.round(3)}"
38
+ { success: true, domain: domain, precision: value }
39
+ end
40
+
41
+ def surprising_errors(**)
42
+ errors = generative_model.surprising_errors
43
+ Legion::Logging.debug "[predictive_coding] surprising_errors count=#{errors.size}"
44
+ { success: true, errors: errors.map(&:to_h), count: errors.size }
45
+ end
46
+
47
+ def free_energy_status(**)
48
+ fe = generative_model.free_energy
49
+ level = generative_model.free_energy_level
50
+ Legion::Logging.debug "[predictive_coding] free_energy_status fe=#{fe.round(3)} level=#{level}"
51
+ {
52
+ success: true,
53
+ free_energy: fe,
54
+ level: level,
55
+ model_stats: generative_model.to_h
56
+ }
57
+ end
58
+
59
+ def active_inference_candidates(**)
60
+ candidates = generative_model.active_inference_candidates
61
+ Legion::Logging.debug "[predictive_coding] active_inference_candidates count=#{candidates.size}"
62
+ { success: true, candidates: candidates, count: candidates.size }
63
+ end
64
+
65
+ def register_active_inference(domain:, action:, expected_outcome:, **)
66
+ inference_id = SecureRandom.uuid
67
+ active_inferences[inference_id] = {
68
+ inference_id: inference_id,
69
+ domain: domain,
70
+ action: action,
71
+ expected_outcome: expected_outcome,
72
+ status: :pending,
73
+ registered_at: Time.now.utc
74
+ }
75
+
76
+ prune_active_inferences
77
+
78
+ Legion::Logging.debug "[predictive_coding] register_active_inference domain=#{domain} id=#{inference_id[0..7]}"
79
+ { success: true, inference_id: inference_id, domain: domain, status: :pending }
80
+ end
81
+
82
+ def resolve_active_inference(domain:, action:, actual_outcome:, inference_id: nil, **)
83
+ record = find_inference(domain, action, inference_id)
84
+ unless record
85
+ Legion::Logging.debug "[predictive_coding] resolve_active_inference not found domain=#{domain}"
86
+ return { success: false, reason: :not_found }
87
+ end
88
+
89
+ expected = record[:expected_outcome]
90
+ error = generative_model.update(
91
+ domain: domain,
92
+ predicted: expected,
93
+ actual: actual_outcome
94
+ )
95
+
96
+ record[:status] = :resolved
97
+ record[:actual_outcome] = actual_outcome
98
+ record[:resolved_at] = Time.now.utc
99
+ record[:error_magnitude] = error.error_magnitude
100
+
101
+ Legion::Logging.info "[predictive_coding] resolve_active_inference domain=#{domain} " \
102
+ "error=#{error.error_magnitude.round(3)} id=#{record[:inference_id][0..7]}"
103
+
104
+ {
105
+ success: true,
106
+ inference_id: record[:inference_id],
107
+ domain: domain,
108
+ error_magnitude: error.error_magnitude,
109
+ action_helpful: error.error_magnitude < Legion::Extensions::PredictiveCoding::Helpers::Constants::SURPRISE_THRESHOLD
110
+ }
111
+ end
112
+
113
+ def update_predictive_coding(**)
114
+ generative_model.decay_all
115
+ pruned = prune_resolved_inferences
116
+ Legion::Logging.debug "[predictive_coding] update_predictive_coding pruned_inferences=#{pruned}"
117
+ { success: true, pruned_inferences: pruned }
118
+ end
119
+
120
+ def predictive_coding_stats(**)
121
+ {
122
+ success: true,
123
+ model: generative_model.to_h,
124
+ active_inferences: active_inferences.size,
125
+ pending_inferences: active_inferences.count { |_, v| v[:status] == :pending }
126
+ }
127
+ end
128
+
129
+ private
130
+
131
+ def generative_model
132
+ @generative_model ||= Helpers::GenerativeModel.new
133
+ end
134
+
135
+ def active_inferences
136
+ @active_inferences ||= {}
137
+ end
138
+
139
+ def prune_active_inferences
140
+ max = Legion::Extensions::PredictiveCoding::Helpers::Constants::MAX_ACTIVE_INFERENCES
141
+ return unless active_inferences.size > max
142
+
143
+ sorted = active_inferences.sort_by { |_, v| v[:registered_at] }
144
+ ids = sorted.first(active_inferences.size - max).map(&:first)
145
+ ids.each { |id| active_inferences.delete(id) }
146
+ end
147
+
148
+ def prune_resolved_inferences
149
+ resolved = active_inferences.select { |_, v| v[:status] == :resolved }.keys
150
+ resolved.each { |id| active_inferences.delete(id) }
151
+ resolved.size
152
+ end
153
+
154
+ def find_inference(domain, action, inference_id)
155
+ if inference_id
156
+ active_inferences[inference_id]
157
+ else
158
+ active_inferences.values.find do |r|
159
+ r[:domain] == domain && r[:action] == action && r[:status] == :pending
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module PredictiveCoding
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'legion/extensions/predictive_coding/version'
5
+ require 'legion/extensions/predictive_coding/helpers/constants'
6
+ require 'legion/extensions/predictive_coding/helpers/prediction_error'
7
+ require 'legion/extensions/predictive_coding/helpers/generative_model'
8
+ require 'legion/extensions/predictive_coding/runners/predictive_coding'
9
+
10
+ module Legion
11
+ module Extensions
12
+ module PredictiveCoding
13
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/predictive_coding/client'
4
+
5
+ RSpec.describe Legion::Extensions::PredictiveCoding::Client do
6
+ subject(:client) { described_class.new }
7
+
8
+ it 'responds to all runner methods' do
9
+ expect(client).to respond_to(:generate_prediction)
10
+ expect(client).to respond_to(:report_outcome)
11
+ expect(client).to respond_to(:precision_for)
12
+ expect(client).to respond_to(:surprising_errors)
13
+ expect(client).to respond_to(:free_energy_status)
14
+ expect(client).to respond_to(:active_inference_candidates)
15
+ expect(client).to respond_to(:register_active_inference)
16
+ expect(client).to respond_to(:resolve_active_inference)
17
+ expect(client).to respond_to(:update_predictive_coding)
18
+ expect(client).to respond_to(:predictive_coding_stats)
19
+ end
20
+
21
+ it 'accepts an injected generative_model' do
22
+ model = Legion::Extensions::PredictiveCoding::Helpers::GenerativeModel.new
23
+ custom_client = described_class.new(generative_model: model)
24
+ expect(custom_client).to respond_to(:generate_prediction)
25
+ end
26
+
27
+ it 'isolates state between separate client instances' do
28
+ client_a = described_class.new
29
+ client_b = described_class.new
30
+
31
+ client_a.report_outcome(domain: :x, predicted: 0.0, actual: 1.0)
32
+ result_b = client_b.surprising_errors
33
+
34
+ expect(result_b[:count]).to eq(0)
35
+ end
36
+
37
+ describe 'full predictive coding lifecycle' do
38
+ it 'predicts, reports outcome, then updates stats correctly' do
39
+ prediction = client.generate_prediction(domain: :proprioception, context: { expected: 0.6 })
40
+ expect(prediction[:success]).to be true
41
+
42
+ outcome = client.report_outcome(domain: :proprioception, predicted: prediction[:predicted], actual: 0.65)
43
+ expect(outcome[:success]).to be true
44
+
45
+ status = client.free_energy_status
46
+ expect(status[:free_energy]).to be_a(Float)
47
+
48
+ stats = client.predictive_coding_stats
49
+ expect(stats[:model][:domain_count]).to eq(1)
50
+ end
51
+
52
+ it 'runs a full active inference cycle' do
53
+ reg = client.register_active_inference(
54
+ domain: :motor_cortex,
55
+ action: :amplify_signal,
56
+ expected_outcome: 0.75
57
+ )
58
+
59
+ expect(reg[:status]).to eq(:pending)
60
+
61
+ resolved = client.resolve_active_inference(
62
+ domain: :motor_cortex,
63
+ action: :amplify_signal,
64
+ actual_outcome: 0.78,
65
+ inference_id: reg[:inference_id]
66
+ )
67
+
68
+ expect(resolved[:success]).to be true
69
+
70
+ after_update = client.update_predictive_coding
71
+ expect(after_update[:pruned_inferences]).to eq(1)
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::PredictiveCoding::Helpers::GenerativeModel do
4
+ let(:model) { described_class.new }
5
+ let(:constants) { Legion::Extensions::PredictiveCoding::Helpers::Constants }
6
+
7
+ describe '#initialize' do
8
+ it 'assigns a model_id' do
9
+ expect(model.model_id).to match(/\A[0-9a-f-]{36}\z/)
10
+ end
11
+
12
+ it 'starts with zero domain models' do
13
+ expect(model.domain_count).to eq(0)
14
+ end
15
+
16
+ it 'starts with zero errors' do
17
+ expect(model.error_count).to eq(0)
18
+ end
19
+
20
+ it 'accepts an explicit model_id' do
21
+ m = described_class.new(model_id: 'custom-id')
22
+ expect(m.model_id).to eq('custom-id')
23
+ end
24
+ end
25
+
26
+ describe '#predict' do
27
+ it 'returns a prediction hash with domain and confidence' do
28
+ result = model.predict(domain: :vision)
29
+ expect(result[:domain]).to eq(:vision)
30
+ expect(result[:predicted]).to be_a(Numeric)
31
+ expect(result[:confidence]).to be_a(Float)
32
+ end
33
+
34
+ it 'uses DEFAULT_PRECISION for an unknown domain' do
35
+ result = model.predict(domain: :new_domain)
36
+ expect(result[:confidence]).to eq(constants::DEFAULT_PRECISION)
37
+ end
38
+
39
+ it 'uses context[:expected] as initial value for unknown domain' do
40
+ result = model.predict(domain: :touch, context: { expected: 0.9 })
41
+ expect(result[:predicted]).to be_within(0.001).of(0.9)
42
+ end
43
+
44
+ it 'improves prediction after learning from updates' do
45
+ 5.times { model.update(domain: :sensor, predicted: 0.5, actual: 0.8) }
46
+ result = model.predict(domain: :sensor)
47
+ expect(result[:predicted]).to be > 0.5
48
+ end
49
+ end
50
+
51
+ describe '#update' do
52
+ it 'returns a PredictionError object' do
53
+ error = model.update(domain: :vision, predicted: 0.5, actual: 0.9)
54
+ expect(error).to be_a(Legion::Extensions::PredictiveCoding::Helpers::PredictionError)
55
+ end
56
+
57
+ it 'increments error count' do
58
+ model.update(domain: :vision, predicted: 0.5, actual: 0.9)
59
+ expect(model.error_count).to eq(1)
60
+ end
61
+
62
+ it 'creates a domain model entry' do
63
+ model.update(domain: :vision, predicted: 0.5, actual: 0.9)
64
+ expect(model.domain_count).to eq(1)
65
+ end
66
+
67
+ it 'adjusts precision downward on high error' do
68
+ initial = model.precision_for(domain: :vision)
69
+ model.update(domain: :vision, predicted: 0.0, actual: 1.0)
70
+ expect(model.precision_for(domain: :vision)).to be < initial
71
+ end
72
+
73
+ it 'adjusts precision upward on zero error' do
74
+ # First update to initialize the domain
75
+ model.update(domain: :vision, predicted: 0.5, actual: 0.5)
76
+ # Second perfect prediction should raise precision
77
+ after_init = model.precision_for(domain: :vision)
78
+ model.update(domain: :vision, predicted: 0.5, actual: 0.5)
79
+ expect(model.precision_for(domain: :vision)).to be >= after_init
80
+ end
81
+ end
82
+
83
+ describe '#precision_for' do
84
+ it 'returns DEFAULT_PRECISION for unknown domain' do
85
+ expect(model.precision_for(domain: :unknown)).to eq(constants::DEFAULT_PRECISION)
86
+ end
87
+
88
+ it 'returns updated precision after updates' do
89
+ model.update(domain: :motor, predicted: 0.5, actual: 0.5)
90
+ expect(model.precision_for(domain: :motor)).to be_a(Float)
91
+ end
92
+ end
93
+
94
+ describe '#free_energy' do
95
+ it 'returns a float' do
96
+ expect(model.free_energy).to be_a(Float)
97
+ end
98
+
99
+ it 'increases after surprising errors' do
100
+ initial = model.free_energy
101
+ 5.times { model.update(domain: :x, predicted: 0.0, actual: 1.0) }
102
+ expect(model.free_energy).to be > initial
103
+ end
104
+ end
105
+
106
+ describe '#free_energy_level' do
107
+ it 'returns a symbol' do
108
+ expect(model.free_energy_level).to be_a(Symbol)
109
+ end
110
+
111
+ it 'returns :minimal for a fresh model' do
112
+ expect(model.free_energy_level).to eq(:minimal)
113
+ end
114
+ end
115
+
116
+ describe '#active_inference_candidates' do
117
+ it 'returns an empty array for a fresh model' do
118
+ expect(model.active_inference_candidates).to eq([])
119
+ end
120
+
121
+ it 'returns domains with high error and low precision' do
122
+ 10.times { model.update(domain: :faulty, predicted: 0.0, actual: 1.0) }
123
+ candidates = model.active_inference_candidates
124
+ expect(candidates).to include(:faulty)
125
+ end
126
+ end
127
+
128
+ describe '#surprising_errors' do
129
+ it 'returns an empty array when no surprising errors' do
130
+ model.update(domain: :x, predicted: 0.5, actual: 0.5)
131
+ expect(model.surprising_errors).to be_empty
132
+ end
133
+
134
+ it 'returns errors above the surprise threshold' do
135
+ model.update(domain: :x, predicted: 0.0, actual: 1.0)
136
+ expect(model.surprising_errors).not_to be_empty
137
+ end
138
+ end
139
+
140
+ describe '#decay_all' do
141
+ it 'decreases precision for all known domains' do
142
+ model.update(domain: :vision, predicted: 0.8, actual: 0.9)
143
+ before = model.precision_for(domain: :vision)
144
+ model.decay_all
145
+ expect(model.precision_for(domain: :vision)).to be <= before
146
+ end
147
+
148
+ it 'does not drop precision below PRECISION_FLOOR' do
149
+ 50.times { model.decay_all }
150
+ model.update(domain: :x, predicted: 0.5, actual: 0.5)
151
+ 50.times { model.decay_all }
152
+ expect(model.precision_for(domain: :x)).to be >= constants::PRECISION_FLOOR
153
+ end
154
+ end
155
+
156
+ describe '#to_h' do
157
+ it 'returns a summary hash' do
158
+ h = model.to_h
159
+ expect(h[:model_id]).to eq(model.model_id)
160
+ expect(h[:domain_count]).to be_a(Integer)
161
+ expect(h[:error_count]).to be_a(Integer)
162
+ expect(h[:free_energy]).to be_a(Float)
163
+ expect(h[:free_energy_level]).to be_a(Symbol)
164
+ expect(h[:domains]).to be_an(Array)
165
+ end
166
+
167
+ it 'includes domain stats after updates' do
168
+ model.update(domain: :vision, predicted: 0.5, actual: 0.7)
169
+ h = model.to_h
170
+ expect(h[:domain_count]).to eq(1)
171
+ vision_stat = h[:domains].find { |d| d[:domain] == :vision }
172
+ expect(vision_stat).not_to be_nil
173
+ expect(vision_stat[:mean]).to be_a(Float)
174
+ end
175
+ end
176
+
177
+ describe 'MAX_MODELS eviction' do
178
+ it 'does not exceed MAX_MODELS domains' do
179
+ (constants::MAX_MODELS + 5).times do |i|
180
+ model.update(domain: :"domain_#{i}", predicted: 0.5, actual: 0.5)
181
+ end
182
+ expect(model.domain_count).to be <= constants::MAX_MODELS
183
+ end
184
+ end
185
+
186
+ describe 'MAX_ERROR_HISTORY cap' do
187
+ it 'caps error history at MAX_ERROR_HISTORY' do
188
+ (constants::MAX_ERROR_HISTORY + 10).times do
189
+ model.update(domain: :x, predicted: 0.5, actual: 0.5)
190
+ end
191
+ expect(model.error_count).to be <= constants::MAX_ERROR_HISTORY
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::PredictiveCoding::Helpers::PredictionError do
4
+ let(:constants) { Legion::Extensions::PredictiveCoding::Helpers::Constants }
5
+
6
+ describe '#initialize' do
7
+ it 'stores domain, predicted, actual' do
8
+ error = described_class.new(domain: :vision, predicted: 0.8, actual: 0.9)
9
+ expect(error.domain).to eq(:vision)
10
+ expect(error.predicted).to eq(0.8)
11
+ expect(error.actual).to eq(0.9)
12
+ end
13
+
14
+ it 'stores a timestamp' do
15
+ error = described_class.new(domain: :vision, predicted: 0.5, actual: 0.5)
16
+ expect(error.timestamp).to be_a(Time)
17
+ end
18
+
19
+ it 'uses DEFAULT_PRECISION when none provided' do
20
+ error = described_class.new(domain: :vision, predicted: 0.5, actual: 0.5)
21
+ expect(error.precision).to eq(constants::DEFAULT_PRECISION)
22
+ end
23
+
24
+ it 'accepts explicit precision' do
25
+ error = described_class.new(domain: :vision, predicted: 0.5, actual: 0.5, precision: 0.9)
26
+ expect(error.precision).to eq(0.9)
27
+ end
28
+ end
29
+
30
+ describe '#error_magnitude' do
31
+ it 'computes absolute difference for numeric values' do
32
+ error = described_class.new(domain: :x, predicted: 0.3, actual: 0.8)
33
+ expect(error.error_magnitude).to be_within(0.001).of(0.5)
34
+ end
35
+
36
+ it 'clamps error magnitude to 1.0 for values differing by more than 1' do
37
+ error = described_class.new(domain: :x, predicted: 0.0, actual: 2.0)
38
+ expect(error.error_magnitude).to eq(1.0)
39
+ end
40
+
41
+ it 'returns 0.0 when predicted equals actual for numeric values' do
42
+ error = described_class.new(domain: :x, predicted: 0.5, actual: 0.5)
43
+ expect(error.error_magnitude).to eq(0.0)
44
+ end
45
+
46
+ it 'returns 0.0 when predicted equals actual for non-numeric values' do
47
+ error = described_class.new(domain: :x, predicted: :foo, actual: :foo)
48
+ expect(error.error_magnitude).to eq(0.0)
49
+ end
50
+
51
+ it 'returns 1.0 when non-numeric predicted differs from actual' do
52
+ error = described_class.new(domain: :x, predicted: :foo, actual: :bar)
53
+ expect(error.error_magnitude).to eq(1.0)
54
+ end
55
+ end
56
+
57
+ describe '#weighted_error' do
58
+ it 'equals error_magnitude * precision' do
59
+ error = described_class.new(domain: :x, predicted: 0.2, actual: 0.6, precision: 0.8)
60
+ expected = (0.6 - 0.2).abs * 0.8
61
+ expect(error.weighted_error).to be_within(0.001).of(expected)
62
+ end
63
+ end
64
+
65
+ describe '#surprising?' do
66
+ it 'returns true when error_magnitude >= SURPRISE_THRESHOLD' do
67
+ error = described_class.new(domain: :x, predicted: 0.0, actual: 1.0)
68
+ expect(error.surprising?).to be true
69
+ end
70
+
71
+ it 'returns false when error_magnitude < SURPRISE_THRESHOLD' do
72
+ error = described_class.new(domain: :x, predicted: 0.5, actual: 0.55)
73
+ expect(error.surprising?).to be false
74
+ end
75
+ end
76
+
77
+ describe '#level' do
78
+ it 'returns :negligible for very small errors' do
79
+ error = described_class.new(domain: :x, predicted: 0.5, actual: 0.505)
80
+ expect(error.level).to eq(:negligible)
81
+ end
82
+
83
+ it 'returns :surprising for large errors' do
84
+ error = described_class.new(domain: :x, predicted: 0.0, actual: 1.0)
85
+ expect(error.level).to eq(:surprising)
86
+ end
87
+
88
+ it 'returns :moderate for mid-range errors' do
89
+ error = described_class.new(domain: :x, predicted: 0.0, actual: 0.4)
90
+ expect(error.level).to eq(:moderate)
91
+ end
92
+ end
93
+
94
+ describe '#to_h' do
95
+ it 'returns a hash with all fields' do
96
+ error = described_class.new(domain: :vision, predicted: 0.3, actual: 0.7, precision: 0.6)
97
+ h = error.to_h
98
+ expect(h[:domain]).to eq(:vision)
99
+ expect(h[:predicted]).to eq(0.3)
100
+ expect(h[:actual]).to eq(0.7)
101
+ expect(h[:error_magnitude]).to be_a(Float)
102
+ expect(h[:precision]).to eq(0.6)
103
+ expect(h[:weighted_error]).to be_a(Float)
104
+ expect(h[:surprising]).to be(true).or be(false)
105
+ expect(h[:level]).to be_a(Symbol)
106
+ expect(h[:timestamp]).to be_a(Time)
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/predictive_coding/client'
4
+
5
+ RSpec.describe Legion::Extensions::PredictiveCoding::Runners::PredictiveCoding do
6
+ let(:client) { Legion::Extensions::PredictiveCoding::Client.new }
7
+
8
+ describe '#generate_prediction' do
9
+ it 'returns success with domain and prediction' do
10
+ result = client.generate_prediction(domain: :vision)
11
+ expect(result[:success]).to be true
12
+ expect(result[:domain]).to eq(:vision)
13
+ expect(result[:predicted]).to be_a(Numeric)
14
+ expect(result[:confidence]).to be_a(Float)
15
+ end
16
+
17
+ it 'passes context to the generative model' do
18
+ result = client.generate_prediction(domain: :audio, context: { expected: 0.7 })
19
+ expect(result[:success]).to be true
20
+ expect(result[:predicted]).to be_within(0.001).of(0.7)
21
+ end
22
+
23
+ it 'accepts extra keyword arguments without error' do
24
+ expect { client.generate_prediction(domain: :x, extra: :ignored) }.not_to raise_error
25
+ end
26
+ end
27
+
28
+ describe '#report_outcome' do
29
+ it 'returns success with error details' do
30
+ result = client.report_outcome(domain: :vision, predicted: 0.5, actual: 0.9)
31
+ expect(result[:success]).to be true
32
+ expect(result[:error_magnitude]).to be_a(Float)
33
+ expect(result[:weighted_error]).to be_a(Float)
34
+ expect(result[:precision]).to be_a(Float)
35
+ expect(result[:surprising]).to be(true).or be(false)
36
+ expect(result[:level]).to be_a(Symbol)
37
+ end
38
+
39
+ it 'marks zero-error outcomes as non-surprising' do
40
+ result = client.report_outcome(domain: :x, predicted: 0.5, actual: 0.5)
41
+ expect(result[:surprising]).to be false
42
+ expect(result[:error_magnitude]).to eq(0.0)
43
+ end
44
+
45
+ it 'marks large-error outcomes as surprising' do
46
+ result = client.report_outcome(domain: :x, predicted: 0.0, actual: 1.0)
47
+ expect(result[:surprising]).to be true
48
+ end
49
+ end
50
+
51
+ describe '#precision_for' do
52
+ it 'returns DEFAULT_PRECISION for unknown domain' do
53
+ result = client.precision_for(domain: :unknown_domain_xyz)
54
+ expect(result[:success]).to be true
55
+ expect(result[:precision]).to eq(Legion::Extensions::PredictiveCoding::Helpers::Constants::DEFAULT_PRECISION)
56
+ end
57
+
58
+ it 'returns updated precision after reporting outcomes' do
59
+ client.report_outcome(domain: :sensor, predicted: 0.5, actual: 0.5)
60
+ result = client.precision_for(domain: :sensor)
61
+ expect(result[:precision]).to be_a(Float)
62
+ end
63
+ end
64
+
65
+ describe '#surprising_errors' do
66
+ it 'returns empty list when no surprising errors' do
67
+ result = client.surprising_errors
68
+ expect(result[:success]).to be true
69
+ expect(result[:errors]).to be_an(Array)
70
+ expect(result[:count]).to eq(0)
71
+ end
72
+
73
+ it 'returns errors above surprise threshold' do
74
+ client.report_outcome(domain: :x, predicted: 0.0, actual: 1.0)
75
+ result = client.surprising_errors
76
+ expect(result[:count]).to be >= 1
77
+ expect(result[:errors].first[:surprising]).to be true
78
+ end
79
+ end
80
+
81
+ describe '#free_energy_status' do
82
+ it 'returns free energy value and level' do
83
+ result = client.free_energy_status
84
+ expect(result[:success]).to be true
85
+ expect(result[:free_energy]).to be_a(Float)
86
+ expect(result[:level]).to be_a(Symbol)
87
+ expect(result[:model_stats]).to be_a(Hash)
88
+ end
89
+
90
+ it 'reflects accumulated errors in free energy' do
91
+ initial = client.free_energy_status[:free_energy]
92
+ 5.times { client.report_outcome(domain: :x, predicted: 0.0, actual: 1.0) }
93
+ elevated = client.free_energy_status[:free_energy]
94
+ expect(elevated).to be > initial
95
+ end
96
+ end
97
+
98
+ describe '#active_inference_candidates' do
99
+ it 'returns empty list for fresh client' do
100
+ result = client.active_inference_candidates
101
+ expect(result[:success]).to be true
102
+ expect(result[:candidates]).to be_an(Array)
103
+ end
104
+
105
+ it 'includes domains with persistent high errors' do
106
+ 10.times { client.report_outcome(domain: :faulty, predicted: 0.0, actual: 1.0) }
107
+ result = client.active_inference_candidates
108
+ expect(result[:candidates]).to include(:faulty)
109
+ end
110
+ end
111
+
112
+ describe '#register_active_inference' do
113
+ it 'registers and returns an inference_id' do
114
+ result = client.register_active_inference(domain: :motor, action: :increase_gain, expected_outcome: 0.8)
115
+ expect(result[:success]).to be true
116
+ expect(result[:inference_id]).to match(/\A[0-9a-f-]{36}\z/)
117
+ expect(result[:status]).to eq(:pending)
118
+ end
119
+
120
+ it 'accepts extra keyword arguments' do
121
+ expect do
122
+ client.register_active_inference(domain: :x, action: :test, expected_outcome: 0.5, extra: :ignored)
123
+ end.not_to raise_error
124
+ end
125
+ end
126
+
127
+ describe '#resolve_active_inference' do
128
+ it 'resolves a registered inference and returns error details' do
129
+ reg = client.register_active_inference(domain: :motor, action: :increase_gain, expected_outcome: 0.8)
130
+ result = client.resolve_active_inference(
131
+ domain: :motor,
132
+ action: :increase_gain,
133
+ actual_outcome: 0.85,
134
+ inference_id: reg[:inference_id]
135
+ )
136
+ expect(result[:success]).to be true
137
+ expect(result[:error_magnitude]).to be_a(Float)
138
+ expect(result).to have_key(:action_helpful)
139
+ end
140
+
141
+ it 'returns not_found when inference does not exist' do
142
+ result = client.resolve_active_inference(
143
+ domain: :missing,
144
+ action: :noop,
145
+ actual_outcome: 0.5,
146
+ inference_id: 'nonexistent-id'
147
+ )
148
+ expect(result[:success]).to be false
149
+ expect(result[:reason]).to eq(:not_found)
150
+ end
151
+
152
+ it 'marks action_helpful true when error is below surprise threshold' do
153
+ reg = client.register_active_inference(domain: :x, action: :nudge, expected_outcome: 0.5)
154
+ result = client.resolve_active_inference(
155
+ domain: :x,
156
+ action: :nudge,
157
+ actual_outcome: 0.51,
158
+ inference_id: reg[:inference_id]
159
+ )
160
+ expect(result[:action_helpful]).to be true
161
+ end
162
+
163
+ it 'marks action_helpful false when error is above surprise threshold' do
164
+ reg = client.register_active_inference(domain: :x, action: :nudge, expected_outcome: 0.0)
165
+ result = client.resolve_active_inference(
166
+ domain: :x,
167
+ action: :nudge,
168
+ actual_outcome: 1.0,
169
+ inference_id: reg[:inference_id]
170
+ )
171
+ expect(result[:action_helpful]).to be false
172
+ end
173
+ end
174
+
175
+ describe '#update_predictive_coding' do
176
+ it 'returns success' do
177
+ result = client.update_predictive_coding
178
+ expect(result[:success]).to be true
179
+ end
180
+
181
+ it 'prunes resolved inferences' do
182
+ reg = client.register_active_inference(domain: :x, action: :test, expected_outcome: 0.5)
183
+ client.resolve_active_inference(domain: :x, action: :test, actual_outcome: 0.5, inference_id: reg[:inference_id])
184
+ result = client.update_predictive_coding
185
+ expect(result[:pruned_inferences]).to be >= 1
186
+ end
187
+
188
+ it 'returns pruned count of zero when nothing resolved' do
189
+ result = client.update_predictive_coding
190
+ expect(result[:pruned_inferences]).to eq(0)
191
+ end
192
+ end
193
+
194
+ describe '#predictive_coding_stats' do
195
+ it 'returns full stats hash' do
196
+ result = client.predictive_coding_stats
197
+ expect(result[:success]).to be true
198
+ expect(result[:model]).to be_a(Hash)
199
+ expect(result[:active_inferences]).to be_a(Integer)
200
+ expect(result[:pending_inferences]).to be_a(Integer)
201
+ end
202
+
203
+ it 'counts active inferences correctly' do
204
+ client.register_active_inference(domain: :x, action: :test, expected_outcome: 0.5)
205
+ result = client.predictive_coding_stats
206
+ expect(result[:active_inferences]).to eq(1)
207
+ expect(result[:pending_inferences]).to eq(1)
208
+ end
209
+ end
210
+ end
@@ -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/predictive_coding'
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,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-predictive-coding
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: Karl Friston's Free Energy Principle / Predictive Processing framework
27
+ for brain-modeled agentic AI
28
+ email:
29
+ - matthewdiverson@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - Gemfile
35
+ - lex-predictive-coding.gemspec
36
+ - lib/legion/extensions/predictive_coding.rb
37
+ - lib/legion/extensions/predictive_coding/actors/decay.rb
38
+ - lib/legion/extensions/predictive_coding/client.rb
39
+ - lib/legion/extensions/predictive_coding/helpers/constants.rb
40
+ - lib/legion/extensions/predictive_coding/helpers/generative_model.rb
41
+ - lib/legion/extensions/predictive_coding/helpers/prediction_error.rb
42
+ - lib/legion/extensions/predictive_coding/runners/predictive_coding.rb
43
+ - lib/legion/extensions/predictive_coding/version.rb
44
+ - spec/legion/extensions/predictive_coding/client_spec.rb
45
+ - spec/legion/extensions/predictive_coding/helpers/generative_model_spec.rb
46
+ - spec/legion/extensions/predictive_coding/helpers/prediction_error_spec.rb
47
+ - spec/legion/extensions/predictive_coding/runners/predictive_coding_spec.rb
48
+ - spec/spec_helper.rb
49
+ homepage: https://github.com/LegionIO/lex-predictive-coding
50
+ licenses:
51
+ - MIT
52
+ metadata:
53
+ homepage_uri: https://github.com/LegionIO/lex-predictive-coding
54
+ source_code_uri: https://github.com/LegionIO/lex-predictive-coding
55
+ documentation_uri: https://github.com/LegionIO/lex-predictive-coding
56
+ changelog_uri: https://github.com/LegionIO/lex-predictive-coding
57
+ bug_tracker_uri: https://github.com/LegionIO/lex-predictive-coding/issues
58
+ rubygems_mfa_required: 'true'
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '3.4'
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubygems_version: 3.6.9
74
+ specification_version: 4
75
+ summary: LEX Predictive Coding
76
+ test_files: []