lex-agentic-inference 0.1.11 → 0.2.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 +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +17 -1
- data/lib/legion/extensions/agentic/inference/bayesian/helpers/belief_network.rb +3 -3
- data/lib/legion/extensions/agentic/inference/bayesian/runners/bayesian_belief.rb +1 -1
- data/lib/legion/extensions/agentic/inference/causal_attribution/helpers/attribution.rb +1 -1
- data/lib/legion/extensions/agentic/inference/magnet/helpers/field.rb +1 -1
- data/lib/legion/extensions/agentic/inference/prediction/client.rb +2 -2
- data/lib/legion/extensions/agentic/inference/prediction/helpers/prediction_store.rb +18 -8
- data/lib/legion/extensions/agentic/inference/prediction/runners/prediction.rb +7 -6
- data/lib/legion/extensions/agentic/inference/version.rb +1 -1
- data/spec/legion/extensions/agentic/inference/bayesian/helpers/belief_network_spec.rb +5 -5
- data/spec/legion/extensions/agentic/inference/prediction/runners/prediction_identity_spec.rb +33 -1
- data/spec/legion/extensions/agentic/inference/prediction/runners/prediction_spec.rb +17 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 82dc037dc5706c8daa4c1a9a3688a5384f9e4312dd8352dca6a0ff2a13892778
|
|
4
|
+
data.tar.gz: 689d77694b89d51bfac49e14a01ff7cc99d4e1c4e5235cfc5dbcd6fc6b6f3bb1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b6aa092e075d7ae2110b1225fe44aa778d647d1a89ccfb421837f1ce06f9ea39dd078dcc594232b6fad8c4246c6421002ccd59bab3882505f77b0c9310bd1e30
|
|
7
|
+
data.tar.gz: eaba3c1bad5020560999363354687dec884c975c8467f8df01cebf4c98fd5872b421ce858d1bb4b2be3a7c36766b7bf514217986695cffdde4cf37480a9907fe
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2026-10-29
|
|
4
|
+
### Changed
|
|
5
|
+
- Default prediction clients and runner instances now share process-local state within the same identity partition. Pass `prediction_stores: {}` to a client when independent state ownership is required.
|
|
6
|
+
### Fixed
|
|
7
|
+
- Make prediction resolution single-use so duplicate or conflicting feedback cannot skew accuracy, overwrite accepted outcomes, or emit repeated outcome traces.
|
|
8
|
+
- Keep wrong-identity lookup, resolution, and erasure isolated. This release does not add restart persistence or infer missing caller identity.
|
|
9
|
+
|
|
3
10
|
## [0.1.11] - 2026-07-23
|
|
4
11
|
### Fixed
|
|
5
12
|
- Move `prediction_store` accessor into `Runners::Prediction` module so it works when extended onto RunnerHost (fixes NoMethodError during GAIA heartbeat tick)
|
data/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Domain consolidation gem for reasoning, inference, and belief management. Bundle
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
7
|
**Gem**: `lex-agentic-inference`
|
|
8
|
-
**Version**: 0.
|
|
8
|
+
**Version**: 0.2.0
|
|
9
9
|
**Namespace**: `Legion::Extensions::Agentic::Inference`
|
|
10
10
|
|
|
11
11
|
## Sub-Modules
|
|
@@ -62,6 +62,22 @@ Domain consolidation gem for reasoning, inference, and belief management. Bundle
|
|
|
62
62
|
gem 'lex-agentic-inference'
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
## Prediction state ownership
|
|
66
|
+
|
|
67
|
+
Default `Prediction::Client` objects and prediction runners share a process-local
|
|
68
|
+
store for each `identity:`. Use the same verified identity for prediction,
|
|
69
|
+
lookup, resolution, and erasure. Omitting identity selects the shared unscoped
|
|
70
|
+
partition; it is not a substitute for principal attribution.
|
|
71
|
+
|
|
72
|
+
For an independent standalone client, pass `prediction_stores: {}` to
|
|
73
|
+
`Legion::Extensions::Agentic::Inference::Prediction::Client.new`. That client's
|
|
74
|
+
stores are not visible to default runtime runners.
|
|
75
|
+
|
|
76
|
+
Only the first resolution of a pending prediction is accepted. Repeated or
|
|
77
|
+
conflicting feedback returns `resolved: false, reason: :already_resolved`;
|
|
78
|
+
unknown IDs in the selected partition return `reason: :not_found`. These
|
|
79
|
+
stores are not durable across process restarts.
|
|
80
|
+
|
|
65
81
|
## Development
|
|
66
82
|
|
|
67
83
|
```bash
|
|
@@ -64,7 +64,7 @@ module Legion
|
|
|
64
64
|
subset.to_h { |b| [b.id, b.posterior / total] }
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
def information_gain(belief_id:,
|
|
67
|
+
def information_gain(belief_id:, likelihood:)
|
|
68
68
|
belief = @beliefs[belief_id]
|
|
69
69
|
return 0.0 unless belief
|
|
70
70
|
|
|
@@ -134,8 +134,8 @@ module Legion
|
|
|
134
134
|
prior_q = 1.0 - prior_p
|
|
135
135
|
post_q = 1.0 - post_p
|
|
136
136
|
|
|
137
|
-
term1 = post_p
|
|
138
|
-
term2 = post_q
|
|
137
|
+
term1 = post_p * Math.log2(post_p / prior_p)
|
|
138
|
+
term2 = post_q * Math.log2(post_q / prior_q)
|
|
139
139
|
term1 + term2
|
|
140
140
|
end
|
|
141
141
|
end
|
|
@@ -77,7 +77,7 @@ module Legion
|
|
|
77
77
|
|
|
78
78
|
def information_gain(belief_id:, evidence_id:, likelihood:, **)
|
|
79
79
|
clamped = likelihood.clamp(Helpers::Constants::LIKELIHOOD_FLOOR, Helpers::Constants::LIKELIHOOD_CEILING)
|
|
80
|
-
gain = belief_network.information_gain(belief_id: belief_id,
|
|
80
|
+
gain = belief_network.information_gain(belief_id: belief_id, likelihood: clamped)
|
|
81
81
|
log.debug "[bayesian_belief] information_gain: id=#{belief_id[0..7]} likelihood=#{clamped.round(3)} gain=#{gain.round(4)}"
|
|
82
82
|
{ success: true, belief_id: belief_id, evidence_id: evidence_id, likelihood: clamped, information_gain: gain }
|
|
83
83
|
end
|
|
@@ -46,7 +46,7 @@ module Legion
|
|
|
46
46
|
@controllability = controllability
|
|
47
47
|
@confidence = confidence.clamp(CONFIDENCE_FLOOR, CONFIDENCE_CEILING)
|
|
48
48
|
@emotional_response = ATTRIBUTION_EMOTIONS[pattern]
|
|
49
|
-
@created_at
|
|
49
|
+
@created_at = Time.now.utc
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
def pattern
|
|
@@ -9,12 +9,20 @@ module Legion
|
|
|
9
9
|
module Prediction
|
|
10
10
|
module Helpers
|
|
11
11
|
class PredictionStore
|
|
12
|
+
@stores = {} # rubocop:disable ThreadSafety/MutableClassInstanceVariable -- Registry access is protected by @stores_mutex.
|
|
13
|
+
@stores_mutex = Mutex.new
|
|
14
|
+
|
|
15
|
+
def self.for_identity(identity:)
|
|
16
|
+
@stores_mutex.synchronize { @stores[identity] ||= new(identity: identity) }
|
|
17
|
+
end
|
|
18
|
+
|
|
12
19
|
attr_reader :predictions, :outcomes, :identity
|
|
13
20
|
|
|
14
21
|
def initialize(identity: nil)
|
|
15
22
|
@identity = identity
|
|
16
23
|
@predictions = {}
|
|
17
24
|
@outcomes = []
|
|
25
|
+
@mutex = Mutex.new
|
|
18
26
|
end
|
|
19
27
|
|
|
20
28
|
def store(prediction)
|
|
@@ -31,16 +39,18 @@ module Legion
|
|
|
31
39
|
end
|
|
32
40
|
|
|
33
41
|
def resolve(prediction_id, outcome:, actual: nil)
|
|
34
|
-
|
|
35
|
-
|
|
42
|
+
@mutex.synchronize do
|
|
43
|
+
pred = @predictions[prediction_id]
|
|
44
|
+
return nil unless pred && pred[:status] == :pending
|
|
36
45
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
pred[:status] = outcome # :correct, :incorrect, :partial, :expired
|
|
47
|
+
pred[:resolved_at] = Time.now.utc
|
|
48
|
+
pred[:actual] = actual
|
|
40
49
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
@outcomes << { prediction_id: prediction_id, outcome: outcome, at: pred[:resolved_at] }
|
|
51
|
+
@outcomes.shift while @outcomes.size > 500
|
|
52
|
+
pred
|
|
53
|
+
end
|
|
44
54
|
end
|
|
45
55
|
|
|
46
56
|
def pending
|
|
@@ -53,8 +53,9 @@ module Legion
|
|
|
53
53
|
record_outcome_trace(pred, outcome)
|
|
54
54
|
{ resolved: true, prediction_id: prediction_id, outcome: outcome }
|
|
55
55
|
else
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
reason = store.get(prediction_id) ? :already_resolved : :not_found
|
|
57
|
+
log.debug "[prediction] resolve failed: #{prediction_id[0..7]} #{reason}"
|
|
58
|
+
{ resolved: false, reason: reason }
|
|
58
59
|
end
|
|
59
60
|
end
|
|
60
61
|
|
|
@@ -81,8 +82,7 @@ module Legion
|
|
|
81
82
|
age = Time.now.utc - pred[:created_at]
|
|
82
83
|
next unless age > pred[:horizon]
|
|
83
84
|
|
|
84
|
-
store.resolve(pred[:prediction_id], outcome: :expired, actual: nil)
|
|
85
|
-
expired_count += 1
|
|
85
|
+
expired_count += 1 if store.resolve(pred[:prediction_id], outcome: :expired, actual: nil)
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
remaining = store.pending.size
|
|
@@ -105,8 +105,9 @@ module Legion
|
|
|
105
105
|
end
|
|
106
106
|
|
|
107
107
|
def prediction_store(identity: nil)
|
|
108
|
-
@prediction_stores ||=
|
|
109
|
-
|
|
108
|
+
return @prediction_stores[identity] ||= Helpers::PredictionStore.new(identity: identity) if @prediction_stores
|
|
109
|
+
|
|
110
|
+
Helpers::PredictionStore.for_identity(identity: identity)
|
|
110
111
|
end
|
|
111
112
|
|
|
112
113
|
def estimate_confidence(mode, context)
|
|
@@ -111,19 +111,19 @@ RSpec.describe Legion::Extensions::Agentic::Inference::Bayesian::Helpers::Belief
|
|
|
111
111
|
describe '#information_gain' do
|
|
112
112
|
it 'returns a non-negative float' do
|
|
113
113
|
belief = add
|
|
114
|
-
gain = network.information_gain(belief_id: belief.id,
|
|
114
|
+
gain = network.information_gain(belief_id: belief.id, likelihood: 0.9)
|
|
115
115
|
expect(gain).to be >= 0.0
|
|
116
116
|
end
|
|
117
117
|
|
|
118
118
|
it 'returns 0.0 for unknown belief' do
|
|
119
|
-
gain = network.information_gain(belief_id: 'nonexistent',
|
|
119
|
+
gain = network.information_gain(belief_id: 'nonexistent', likelihood: 0.8)
|
|
120
120
|
expect(gain).to eq(0.0)
|
|
121
121
|
end
|
|
122
122
|
|
|
123
123
|
it 'does not mutate the belief posterior' do
|
|
124
124
|
belief = add
|
|
125
125
|
before = belief.posterior
|
|
126
|
-
network.information_gain(belief_id: belief.id,
|
|
126
|
+
network.information_gain(belief_id: belief.id, likelihood: 0.9)
|
|
127
127
|
expect(belief.posterior).to eq(before)
|
|
128
128
|
end
|
|
129
129
|
|
|
@@ -131,8 +131,8 @@ RSpec.describe Legion::Extensions::Agentic::Inference::Bayesian::Helpers::Belief
|
|
|
131
131
|
b1 = add(content: 'h1')
|
|
132
132
|
b2 = add(content: 'h2')
|
|
133
133
|
|
|
134
|
-
gain_strong = network.information_gain(belief_id: b1.id,
|
|
135
|
-
gain_weak = network.information_gain(belief_id: b2.id,
|
|
134
|
+
gain_strong = network.information_gain(belief_id: b1.id, likelihood: 0.95)
|
|
135
|
+
gain_weak = network.information_gain(belief_id: b2.id, likelihood: 0.55)
|
|
136
136
|
expect(gain_strong).to be > gain_weak
|
|
137
137
|
end
|
|
138
138
|
end
|
data/spec/legion/extensions/agentic/inference/prediction/runners/prediction_identity_spec.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require 'legion/extensions/agentic/inference/prediction/client'
|
|
4
4
|
|
|
5
5
|
RSpec.describe Legion::Extensions::Agentic::Inference::Prediction::Runners::Prediction do
|
|
6
|
-
let(:client) { Legion::Extensions::Agentic::Inference::Prediction::Client.new }
|
|
6
|
+
let(:client) { Legion::Extensions::Agentic::Inference::Prediction::Client.new(prediction_stores: {}) }
|
|
7
7
|
|
|
8
8
|
describe 'per-identity isolation' do
|
|
9
9
|
it 'predictions for identity X are not visible to identity Y' do
|
|
@@ -42,6 +42,38 @@ RSpec.describe Legion::Extensions::Agentic::Inference::Prediction::Runners::Pred
|
|
|
42
42
|
end
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
describe 'shared runtime state' do
|
|
46
|
+
let(:identity) { SecureRandom.uuid }
|
|
47
|
+
let(:other_identity) { SecureRandom.uuid }
|
|
48
|
+
let(:producer) { Legion::Extensions::Agentic::Inference::Prediction::Client.new }
|
|
49
|
+
let(:consumer) { Object.new.extend(described_class) }
|
|
50
|
+
|
|
51
|
+
after do
|
|
52
|
+
producer.erase_partner!(identity: identity)
|
|
53
|
+
producer.erase_partner!(identity: other_identity)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'resolves a client prediction through separate runner instances without crossing identities' do
|
|
57
|
+
prediction = producer.predict(mode: :functional_mapping, confidence: 0.9, identity: identity)
|
|
58
|
+
other = producer.predict(mode: :functional_mapping, identity: other_identity)
|
|
59
|
+
|
|
60
|
+
expect(consumer.pending_predictions(identity: identity)[:predictions].map { |entry| entry[:prediction_id] })
|
|
61
|
+
.to eq([prediction[:prediction_id]])
|
|
62
|
+
expect(consumer.resolve_prediction(prediction_id: prediction[:prediction_id], outcome: :incorrect,
|
|
63
|
+
identity: other_identity)).to include(resolved: false, reason: :not_found)
|
|
64
|
+
|
|
65
|
+
resolver = Object.new.extend(described_class)
|
|
66
|
+
resolver.resolve_prediction(prediction_id: prediction[:prediction_id], outcome: :correct, identity: identity)
|
|
67
|
+
|
|
68
|
+
expect(producer.prediction_accuracy(identity: identity)).to eq(accuracy: 1.0, total_outcomes: 1)
|
|
69
|
+
expect(producer.pending_predictions(identity: identity)[:predictions]).to be_empty
|
|
70
|
+
expect(consumer.get_prediction(prediction_id: other[:prediction_id], identity: other_identity)[:prediction])
|
|
71
|
+
.to include(status: :pending)
|
|
72
|
+
consumer.erase_partner!(identity: identity)
|
|
73
|
+
expect(producer.get_prediction(prediction_id: prediction[:prediction_id], identity: identity)[:found]).to be false
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
45
77
|
describe '#erase_partner!' do
|
|
46
78
|
it 'removes all predictions and outcomes for the identity' do
|
|
47
79
|
3.times do
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require 'legion/extensions/agentic/inference/prediction/client'
|
|
4
4
|
|
|
5
5
|
RSpec.describe Legion::Extensions::Agentic::Inference::Prediction::Runners::Prediction do
|
|
6
|
-
let(:client) { Legion::Extensions::Agentic::Inference::Prediction::Client.new }
|
|
6
|
+
let(:client) { Legion::Extensions::Agentic::Inference::Prediction::Client.new(prediction_stores: {}) }
|
|
7
7
|
|
|
8
8
|
describe '#predict' do
|
|
9
9
|
it 'creates a prediction with valid mode' do
|
|
@@ -45,6 +45,22 @@ RSpec.describe Legion::Extensions::Agentic::Inference::Prediction::Runners::Pred
|
|
|
45
45
|
result = client.resolve_prediction(prediction_id: 'nonexistent', outcome: :correct)
|
|
46
46
|
expect(result[:resolved]).to be false
|
|
47
47
|
end
|
|
48
|
+
|
|
49
|
+
it 'counts a prediction outcome once and preserves the first resolution' do
|
|
50
|
+
correct = client.predict(mode: :functional_mapping)
|
|
51
|
+
incorrect = client.predict(mode: :functional_mapping)
|
|
52
|
+
client.resolve_prediction(prediction_id: correct[:prediction_id], outcome: :correct, actual: 'observed')
|
|
53
|
+
client.resolve_prediction(prediction_id: incorrect[:prediction_id], outcome: :incorrect)
|
|
54
|
+
|
|
55
|
+
repeated = client.resolve_prediction(prediction_id: correct[:prediction_id], outcome: :correct)
|
|
56
|
+
conflicting = client.resolve_prediction(prediction_id: correct[:prediction_id], outcome: :incorrect)
|
|
57
|
+
|
|
58
|
+
expect(repeated).to include(resolved: false, reason: :already_resolved)
|
|
59
|
+
expect(conflicting).to include(resolved: false, reason: :already_resolved)
|
|
60
|
+
expect(client.prediction_accuracy).to eq(accuracy: 0.5, total_outcomes: 2)
|
|
61
|
+
expect(client.get_prediction(prediction_id: correct[:prediction_id])[:prediction])
|
|
62
|
+
.to include(status: :correct, actual: 'observed')
|
|
63
|
+
end
|
|
48
64
|
end
|
|
49
65
|
|
|
50
66
|
describe '#pending_predictions' do
|