Module: Legion::Gaia::Anticipation

Extended by:
Logging::Helper
Defined in:
lib/legion/gaia/anticipation.rb

Overview

H6 — prediction pre-staging with emotional modulation (§12.8).

Reads pending predictions from lex-agentic-inference (if available). Actionable predictions (confidence >= threshold) trigger pre-staged RAG retrieval, capped at 25% of total entries.

Emotional modulation: negative-valence anticipations defer when the partner's affect baseline is low (soft-guarded on lex-agentic-affect).

Entropy guard: no resolution under high identity entropy (consistent with H2). Soft-guarded throughout: returns nil when dependencies are absent.

Constant Summary collapse

LOW_AFFECT_BASELINE =

Threshold below which the partner's affect baseline is considered "low".

0.3
PRESTAGE_CAP_RATIO =

Cap on pre-staged entries as a fraction of total RAG entries.

0.25

Class Method Summary collapse

Class Method Details

.actionable_thresholdObject



140
141
142
143
144
145
146
# File 'lib/legion/gaia/anticipation.rb', line 140

def actionable_threshold
  settings = Legion::Gaia.settings
  settings&.dig(:anticipation, :actionable_threshold) ||
    (defined?(Legion::Extensions::Agentic::Inference::Prediction::Helpers::Modes::PREDICTION_CONFIDENCE_MIN) &&
     Legion::Extensions::Agentic::Inference::Prediction::Helpers::Modes::PREDICTION_CONFIDENCE_MIN) ||
    0.65
end

.affect_baseline_low?(identity:) ⇒ Boolean

Returns true when the partner's affect baseline is below LOW_AFFECT_BASELINE.

Returns:

  • (Boolean)


200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/legion/gaia/anticipation.rb', line 200

def affect_baseline_low?(identity:)
  return false unless defined?(Legion::Extensions::Agentic::Affect::Empathy::Helpers::ModelStore)

  model_store = Legion::Extensions::Agentic::Affect::Empathy::Helpers::ModelStore.new
  model = model_store.get(identity)
  return false unless model.respond_to?(:emotional_state)

  negative_states = %i[stressed anxious frustrated overwhelmed sad]
  negative_states.include?(model.emotional_state)
rescue StandardError => e
  handle_exception(e, level: :debug, operation: 'gaia.anticipation.affect_baseline_low', identity: identity)
  false
end

.build(identity:, context:) ⇒ Hash?

Build anticipation context for advisory.

Parameters:

  • identity (String)

    partner identity

  • context (Hash)

    current request context, e.g. { rag_entry_count:, entropy: }

Returns:

  • (Hash, nil)

    { prediction_id:, content:, confidence:, pre_staged: [...] } nil when lex-agentic-inference not loaded or no actionable predictions



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/legion/gaia/anticipation.rb', line 37

def build(identity:, context:)
  return nil unless inference_available?

  runner = inference_runner
  result = runner.pending_predictions(identity: identity.to_s)
  predictions = Array(result[:predictions])
  return nil if predictions.empty?

  actionable = predictions.select { |p| p[:confidence].to_f >= actionable_threshold }
  return nil if actionable.empty?

  # Pick the highest-confidence actionable prediction
  best = actionable.max_by { |p| p[:confidence].to_f }

  # Emotional modulation: defer negative-valence anticipation when affect baseline is low
  if deferred?(identity: identity, anticipation: best)
    log.info("[anticipation] deferred identity=#{identity} prediction_id=#{best[:prediction_id].to_s[0, 8]} " \
             'reason=emotional_state')
    return nil
  end

  pre_staged = build_prestaged(context: context, prediction: best)

  log.info("[anticipation] built identity=#{identity} prediction_id=#{best[:prediction_id].to_s[0, 8]} " \
           "confidence=#{best[:confidence].to_f.round(3)} pre_staged=#{pre_staged.size}")

  {
    prediction_id: best[:prediction_id],
    content: best[:description] || best[:context].to_s,
    confidence: best[:confidence].to_f,
    pre_staged: pre_staged
  }
rescue StandardError => e
  handle_exception(e, level: :warn, operation: 'gaia.anticipation.build', identity: identity)
  nil
end

.build_prestaged(context:, prediction:) ⇒ Object

Builds pre-staged RAG entries capped at 25% of total context entries.



149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/legion/gaia/anticipation.rb', line 149

def build_prestaged(context:, prediction:)
  return [] unless context.is_a?(Hash)

  rag_count = context[:rag_entry_count].to_i
  return [] if rag_count.zero?

  cap = [(rag_count * PRESTAGE_CAP_RATIO).ceil, 1].max
  prediction_context = prediction[:context]
  return [] unless prediction_context.is_a?(Hash) && prediction_context[:pre_stage_entries].is_a?(Array)

  prediction_context[:pre_stage_entries].first(cap)
end

.deferred?(identity:, anticipation:) ⇒ Boolean

Check if an anticipation should be deferred due to emotional state.

Deferred when the anticipation has negative emotional context AND the partner's current affect baseline is low (§12.8 emotional modulation).

Parameters:

  • identity (String)

    partner identity

  • anticipation (Hash)

    prediction entry with optional :emotional_context

Returns:

  • (Boolean)


118
119
120
121
122
123
124
125
126
127
128
# File 'lib/legion/gaia/anticipation.rb', line 118

def deferred?(identity:, anticipation:)
  return false unless anticipation.is_a?(Hash)

  anticipation_valence = extract_valence(anticipation)
  return false unless anticipation_valence < 0.0

  affect_baseline_low?(identity: identity.to_s)
rescue StandardError => e
  handle_exception(e, level: :debug, operation: 'gaia.anticipation.deferred', identity: identity)
  false
end

.extract_valence(anticipation) ⇒ Object



214
215
216
217
218
219
220
221
222
223
# File 'lib/legion/gaia/anticipation.rb', line 214

def extract_valence(anticipation)
  ctx = anticipation[:emotional_context]
  if ctx.is_a?(Hash)
    ctx[:valence].to_f
  elsif ctx.is_a?(Numeric)
    ctx.to_f
  else
    0.0
  end
end

.grade_prediction(prediction_id:, actual:, identity:) ⇒ Object

Grade a prediction as :correct, :incorrect, or :partial.



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/legion/gaia/anticipation.rb', line 163

def grade_prediction(prediction_id:, actual:, identity:)
  return :incorrect unless inference_available?

  runner = inference_runner
  pred_result = runner.get_prediction(prediction_id: prediction_id, identity: identity)
  return :incorrect unless pred_result[:found]

  prediction = pred_result[:prediction]
  predicted = prediction[:description].to_s.downcase.strip
  actual_str = actual.to_s.downcase.strip

  return :incorrect if predicted.empty? || actual_str.empty?

  if actual_str.include?(predicted) || predicted.include?(actual_str)
    :correct
  elsif partial_match?(predicted, actual_str)
    :partial
  else
    :incorrect
  end
rescue StandardError => e
  handle_exception(e, level: :debug, operation: 'gaia.anticipation.grade_prediction',
                      prediction_id: prediction_id)
  :incorrect
end

.high_entropy?(identity:) ⇒ Boolean

Returns:

  • (Boolean)


225
226
227
228
229
230
231
232
233
234
235
# File 'lib/legion/gaia/anticipation.rb', line 225

def high_entropy?(identity:)
  return false unless defined?(Legion::Extensions::Agentic::Self::Helpers::IdentityFingerprint)

  runner = Object.new
  runner.extend(Legion::Extensions::Agentic::Self::Helpers::IdentityFingerprint)
  entropy = runner.identity_entropy(identity: identity)
  threshold = Legion::Gaia.settings&.dig(:identity, :high_entropy_threshold) || 0.7
  entropy.to_f >= threshold
rescue StandardError
  false
end

.inference_available?Boolean

Returns:

  • (Boolean)


130
131
132
# File 'lib/legion/gaia/anticipation.rb', line 130

def inference_available?
  defined?(Legion::Extensions::Agentic::Inference::Prediction::Runners::Prediction) == 'constant'
end

.inference_runnerObject



134
135
136
137
138
# File 'lib/legion/gaia/anticipation.rb', line 134

def inference_runner
  runner = Object.new
  runner.extend(Legion::Extensions::Agentic::Inference::Prediction::Runners::Prediction)
  runner
end

.partial_match?(predicted, actual) ⇒ Boolean

Returns:

  • (Boolean)


189
190
191
192
193
194
195
196
197
# File 'lib/legion/gaia/anticipation.rb', line 189

def partial_match?(predicted, actual)
  pred_words = predicted.split(/\s+/).reject(&:empty?)
  actual_words = actual.split(/\s+/).reject(&:empty?)
  return false if pred_words.empty? || actual_words.empty?

  overlap = pred_words & actual_words
  ratio = overlap.size.to_f / [pred_words.size, actual_words.size].min
  ratio >= 0.5
end

.resolve(identity:, prediction_id:, actual:) ⇒ Hash?

Resolve a pending anticipation against the partner's actual next input.

Grades the prediction as :correct, :incorrect, or :partial and records the outcome. Skipped under high identity entropy.

Parameters:

  • identity (String)

    partner identity

  • prediction_id (String)

    UUID of the prediction to resolve

  • actual (String)

    the partner's actual input

Returns:

  • (Hash, nil)

    { resolved: true/false, outcome:, prediction_id: } nil when dependencies unavailable



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/legion/gaia/anticipation.rb', line 85

def resolve(identity:, prediction_id:, actual:)
  return nil unless inference_available?

  identity_str = identity.to_s

  if high_entropy?(identity: identity_str)
    log.info("[anticipation] resolve skipped identity=#{identity_str} reason=high_entropy")
    return { resolved: false, reason: :high_entropy, prediction_id: prediction_id }
  end

  outcome = grade_prediction(prediction_id: prediction_id, actual: actual, identity: identity_str)
  runner = inference_runner
  result = runner.resolve_prediction(prediction_id: prediction_id, outcome: outcome,
                                     actual: actual, identity: identity_str)

  log.info("[anticipation] resolved identity=#{identity_str} prediction_id=#{prediction_id.to_s[0, 8]} " \
           "outcome=#{outcome}")

  result
rescue StandardError => e
  handle_exception(e, level: :warn, operation: 'gaia.anticipation.resolve',
                      identity: identity, prediction_id: prediction_id)
  nil
end