lex-attention-schema 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 +7 -0
- data/Gemfile +11 -0
- data/lex-attention-schema.gemspec +32 -0
- data/lib/legion/extensions/attention_schema/actors/decay.rb +41 -0
- data/lib/legion/extensions/attention_schema/client.rb +24 -0
- data/lib/legion/extensions/attention_schema/helpers/attention_schema_model.rb +222 -0
- data/lib/legion/extensions/attention_schema/helpers/constants.rb +62 -0
- data/lib/legion/extensions/attention_schema/helpers/schema_item.rb +64 -0
- data/lib/legion/extensions/attention_schema/runners/attention_schema.rb +113 -0
- data/lib/legion/extensions/attention_schema/version.rb +9 -0
- data/lib/legion/extensions/attention_schema.rb +16 -0
- data/spec/legion/extensions/attention_schema/client_spec.rb +47 -0
- data/spec/legion/extensions/attention_schema/helpers/attention_schema_model_spec.rb +219 -0
- data/spec/legion/extensions/attention_schema/helpers/schema_item_spec.rb +114 -0
- data/spec/legion/extensions/attention_schema/runners/attention_schema_spec.rb +185 -0
- data/spec/spec_helper.rb +20 -0
- metadata +78 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 26066d98d084abc8e93fce8bfe09e7ef1e5c9ce0f26178264c869700de98f425
|
|
4
|
+
data.tar.gz: 2e9e273d38e278b3798d426c778fe030e4819edd1197a8ecf88f7ced1f16d64e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 856518fd4426d3a60e16a1616c054225fa949e7ad839da093f07c321dd538e258cdbcf1c10143e84b57a808f0cc62619a1c9731ccaff71d1e5cc4733e374f258
|
|
7
|
+
data.tar.gz: 99e3002b9ee3cb8260cdec4d4c3a144c0949d43327a889144209ebf57cdcbb126b1b8ea383bb658f5f4cb40c8e859bda546329751a6498f81df6dec5e2bd21e1
|
data/Gemfile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/attention_schema/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'lex-attention-schema'
|
|
7
|
+
spec.version = Legion::Extensions::AttentionSchema::VERSION
|
|
8
|
+
spec.authors = ['Esity']
|
|
9
|
+
spec.email = ['matthewdiverson@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'LEX Attention Schema'
|
|
12
|
+
spec.description = "Graziano's Attention Schema Theory for brain-modeled agentic AI — the agent " \
|
|
13
|
+
'maintains a simplified internal model of its own attention process, enabling ' \
|
|
14
|
+
'awareness attribution, social attention modeling, meta-attention monitoring, ' \
|
|
15
|
+
'and natural-language attention reports.'
|
|
16
|
+
spec.homepage = 'https://github.com/LegionIO/lex-attention-schema'
|
|
17
|
+
spec.license = 'MIT'
|
|
18
|
+
spec.required_ruby_version = '>= 3.4'
|
|
19
|
+
|
|
20
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
21
|
+
spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-attention-schema'
|
|
22
|
+
spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-attention-schema'
|
|
23
|
+
spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-attention-schema'
|
|
24
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-attention-schema/issues'
|
|
25
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
26
|
+
|
|
27
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
28
|
+
Dir.glob('{lib,spec}/**/*') + %w[lex-attention-schema.gemspec Gemfile]
|
|
29
|
+
end
|
|
30
|
+
spec.require_paths = ['lib']
|
|
31
|
+
spec.add_development_dependency 'legion-gaia'
|
|
32
|
+
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 AttentionSchema
|
|
8
|
+
module Actor
|
|
9
|
+
class Decay < Legion::Extensions::Actors::Every
|
|
10
|
+
def runner_class
|
|
11
|
+
Legion::Extensions::AttentionSchema::Runners::AttentionSchema
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def runner_function
|
|
15
|
+
'decay_schema'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def time
|
|
19
|
+
30
|
|
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/attention_schema/helpers/constants'
|
|
4
|
+
require 'legion/extensions/attention_schema/helpers/schema_item'
|
|
5
|
+
require 'legion/extensions/attention_schema/helpers/attention_schema_model'
|
|
6
|
+
require 'legion/extensions/attention_schema/runners/attention_schema'
|
|
7
|
+
|
|
8
|
+
module Legion
|
|
9
|
+
module Extensions
|
|
10
|
+
module AttentionSchema
|
|
11
|
+
class Client
|
|
12
|
+
include Runners::AttentionSchema
|
|
13
|
+
|
|
14
|
+
def initialize(schema_model: nil, **)
|
|
15
|
+
@schema_model = schema_model || Helpers::AttentionSchemaModel.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
attr_reader :schema_model
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module AttentionSchema
|
|
6
|
+
module Helpers
|
|
7
|
+
class AttentionSchemaModel
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
attr_reader :schema_items, :social_models, :meta_accuracy, :attention_history
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@schema_items = {} # target => SchemaItem
|
|
14
|
+
@social_models = {} # agent_id => { target:, awareness:, updated_at: }
|
|
15
|
+
@meta_accuracy = 0.5 # EMA confidence in schema self-accuracy
|
|
16
|
+
@attention_history = [] # ring buffer of { target:, event:, at: }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# --- Focus Management ---
|
|
20
|
+
|
|
21
|
+
# Add or boost an item in the attention schema
|
|
22
|
+
def focus_on(target:, domain:, reason:, source:)
|
|
23
|
+
target = target.to_s
|
|
24
|
+
if @schema_items.key?(target)
|
|
25
|
+
@schema_items[target].boost
|
|
26
|
+
record_history(target, :refocused)
|
|
27
|
+
else
|
|
28
|
+
prune_to_capacity if @schema_items.size >= MAX_SCHEMA_ITEMS
|
|
29
|
+
@schema_items[target] = SchemaItem.new(
|
|
30
|
+
target: target,
|
|
31
|
+
domain: domain,
|
|
32
|
+
reason: reason,
|
|
33
|
+
source: source,
|
|
34
|
+
awareness_level: DEFAULT_AWARENESS + AWARENESS_BOOST
|
|
35
|
+
)
|
|
36
|
+
record_history(target, :focused)
|
|
37
|
+
end
|
|
38
|
+
@schema_items[target]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Remove an item from the schema entirely
|
|
42
|
+
def defocus(target:)
|
|
43
|
+
target = target.to_s
|
|
44
|
+
removed = @schema_items.delete(target)
|
|
45
|
+
record_history(target, :defocused) if removed
|
|
46
|
+
!removed.nil?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# --- Awareness Query (core AST operation) ---
|
|
50
|
+
|
|
51
|
+
# "Am I aware of X?" — the central Graziano query
|
|
52
|
+
def am_i_aware_of(target:)
|
|
53
|
+
item = @schema_items[target.to_s]
|
|
54
|
+
return { aware: false, awareness_level: 0.0, label: :unconscious } unless item
|
|
55
|
+
|
|
56
|
+
{ aware: item.awareness_level > AWARENESS_FLOOR, awareness_level: item.awareness_level.round(4), label: item.label }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# --- Attention Report ---
|
|
60
|
+
|
|
61
|
+
# Natural-language-style summary of current attention state
|
|
62
|
+
def report_awareness
|
|
63
|
+
state = attention_state
|
|
64
|
+
state_str = ATTENTION_STATE_LABELS[state] || 'in an unknown state'
|
|
65
|
+
top_items = top_schema_items(3)
|
|
66
|
+
|
|
67
|
+
return { state: state, state_label: state_str, report: 'No active attention targets.', items: [] } if top_items.empty?
|
|
68
|
+
|
|
69
|
+
primary = top_items.first
|
|
70
|
+
summary = "I am #{state_str}, primarily attending to '#{primary[:target]}' " \
|
|
71
|
+
"(#{primary[:label]}, #{primary[:awareness_level]}). " \
|
|
72
|
+
"Reason: #{primary[:reason]}."
|
|
73
|
+
|
|
74
|
+
if top_items.size > 1
|
|
75
|
+
secondary = top_items[1..]
|
|
76
|
+
.map { |i| "'#{i[:target]}' (#{i[:label]})" }
|
|
77
|
+
.join(', ')
|
|
78
|
+
summary += " Also attending to: #{secondary}."
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
{ state: state, state_label: state_str, report: summary, items: top_items }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# --- Attention State Classification ---
|
|
85
|
+
|
|
86
|
+
# Overall qualitative attention state
|
|
87
|
+
def attention_state
|
|
88
|
+
return :distracted if @schema_items.empty?
|
|
89
|
+
|
|
90
|
+
top = top_awareness
|
|
91
|
+
avg = average_awareness
|
|
92
|
+
|
|
93
|
+
if top >= HYPERFOCUS_THRESHOLD
|
|
94
|
+
:hyperfocused
|
|
95
|
+
elsif avg < DRIFT_THRESHOLD && @schema_items.size > 3
|
|
96
|
+
:distracted
|
|
97
|
+
elsif avg < DRIFT_THRESHOLD
|
|
98
|
+
:drifting
|
|
99
|
+
elsif top >= 0.6
|
|
100
|
+
:focused
|
|
101
|
+
else
|
|
102
|
+
:normal
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# --- Social Attention Modeling ---
|
|
107
|
+
|
|
108
|
+
# Record what another agent appears to be attending to
|
|
109
|
+
def model_other_attention(agent_id:, target:, awareness:)
|
|
110
|
+
agent_id = agent_id.to_s
|
|
111
|
+
prune_social_models if @social_models.size >= MAX_SOCIAL_MODELS && !@social_models.key?(agent_id)
|
|
112
|
+
@social_models[agent_id] = {
|
|
113
|
+
target: target.to_s,
|
|
114
|
+
awareness: awareness.clamp(0.0, 1.0),
|
|
115
|
+
updated_at: Time.now.utc
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Query what another agent is modeled as attending to
|
|
120
|
+
def query_other_attention(agent_id:)
|
|
121
|
+
@social_models[agent_id.to_s]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# --- Meta-Attention ---
|
|
125
|
+
|
|
126
|
+
# Assess whether meta-attention signals are active
|
|
127
|
+
def meta_check
|
|
128
|
+
state = attention_state
|
|
129
|
+
signals = []
|
|
130
|
+
|
|
131
|
+
signals << :drifting if %i[drifting distracted].include?(state)
|
|
132
|
+
signals << :hyperfocus if state == :hyperfocused
|
|
133
|
+
signals << :normal if signals.empty?
|
|
134
|
+
|
|
135
|
+
top = top_awareness
|
|
136
|
+
avg = average_awareness
|
|
137
|
+
|
|
138
|
+
{
|
|
139
|
+
state: state,
|
|
140
|
+
signals: signals,
|
|
141
|
+
top_awareness: top.round(4),
|
|
142
|
+
avg_awareness: avg.round(4),
|
|
143
|
+
schema_size: @schema_items.size,
|
|
144
|
+
meta_accuracy: @meta_accuracy.round(4)
|
|
145
|
+
}
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Update the EMA tracking how well the schema predicted actual attention
|
|
149
|
+
def update_meta_accuracy(was_correct:)
|
|
150
|
+
correction = was_correct ? 1.0 : 0.0
|
|
151
|
+
@meta_accuracy += (META_ATTENTION_ALPHA * (correction - @meta_accuracy))
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# --- Decay ---
|
|
155
|
+
|
|
156
|
+
# Apply per-tick decay to all schema items and prune faded ones
|
|
157
|
+
def decay_all
|
|
158
|
+
@schema_items.each_value(&:decay)
|
|
159
|
+
@schema_items.reject! { |_, item| item.faded? }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# --- Accessors ---
|
|
163
|
+
|
|
164
|
+
def schema_size
|
|
165
|
+
@schema_items.size
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def to_h
|
|
169
|
+
{
|
|
170
|
+
state: attention_state,
|
|
171
|
+
state_label: ATTENTION_STATE_LABELS[attention_state],
|
|
172
|
+
schema_size: schema_size,
|
|
173
|
+
meta_accuracy: @meta_accuracy.round(4),
|
|
174
|
+
top_awareness: top_awareness.round(4),
|
|
175
|
+
avg_awareness: average_awareness.round(4),
|
|
176
|
+
social_models: @social_models.size,
|
|
177
|
+
items: @schema_items.values.map(&:to_h)
|
|
178
|
+
}
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
private
|
|
182
|
+
|
|
183
|
+
def top_awareness
|
|
184
|
+
return 0.0 if @schema_items.empty?
|
|
185
|
+
|
|
186
|
+
@schema_items.values.map(&:awareness_level).max
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def average_awareness
|
|
190
|
+
return 0.0 if @schema_items.empty?
|
|
191
|
+
|
|
192
|
+
vals = @schema_items.values.map(&:awareness_level)
|
|
193
|
+
vals.sum / vals.size
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def top_schema_items(n = 3)
|
|
197
|
+
@schema_items.values
|
|
198
|
+
.sort_by { |i| -i.awareness_level }
|
|
199
|
+
.first(n)
|
|
200
|
+
.map(&:to_h)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def prune_to_capacity
|
|
204
|
+
# Remove the item with the lowest awareness
|
|
205
|
+
weakest = @schema_items.min_by { |_, item| item.awareness_level }&.first
|
|
206
|
+
@schema_items.delete(weakest) if weakest
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def prune_social_models
|
|
210
|
+
oldest = @social_models.min_by { |_, v| v[:updated_at] }&.first
|
|
211
|
+
@social_models.delete(oldest) if oldest
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def record_history(target, event)
|
|
215
|
+
@attention_history << { target: target, event: event, at: Time.now.utc }
|
|
216
|
+
@attention_history.shift while @attention_history.size > MAX_HISTORY
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module AttentionSchema
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
# Maximum items the schema can model simultaneously
|
|
9
|
+
MAX_SCHEMA_ITEMS = 15
|
|
10
|
+
|
|
11
|
+
# Maximum other agents whose attention we model socially
|
|
12
|
+
MAX_SOCIAL_MODELS = 10
|
|
13
|
+
|
|
14
|
+
# EMA alpha for schema update speed
|
|
15
|
+
SCHEMA_UPDATE_ALPHA = 0.15
|
|
16
|
+
|
|
17
|
+
# Default awareness level for newly focused items
|
|
18
|
+
DEFAULT_AWARENESS = 0.3
|
|
19
|
+
|
|
20
|
+
# Minimum awareness before item is pruned from schema
|
|
21
|
+
AWARENESS_FLOOR = 0.05
|
|
22
|
+
|
|
23
|
+
# Per-tick decay applied to all schema items
|
|
24
|
+
AWARENESS_DECAY = 0.02
|
|
25
|
+
|
|
26
|
+
# Awareness boost applied when re-focusing an existing item
|
|
27
|
+
AWARENESS_BOOST = 0.15
|
|
28
|
+
|
|
29
|
+
# Below this awareness average: attention is drifting
|
|
30
|
+
DRIFT_THRESHOLD = 0.3
|
|
31
|
+
|
|
32
|
+
# Above this awareness (for top item): attention is hyper-focused
|
|
33
|
+
HYPERFOCUS_THRESHOLD = 0.85
|
|
34
|
+
|
|
35
|
+
# EMA alpha for meta-attention accuracy tracking
|
|
36
|
+
META_ATTENTION_ALPHA = 0.1
|
|
37
|
+
|
|
38
|
+
# Maximum entries kept in attention history ring buffer
|
|
39
|
+
MAX_HISTORY = 200
|
|
40
|
+
|
|
41
|
+
# Human-readable labels keyed by awareness level range
|
|
42
|
+
AWARENESS_LABELS = {
|
|
43
|
+
(0.8..) => :vivid,
|
|
44
|
+
(0.6...0.8) => :clear,
|
|
45
|
+
(0.4...0.6) => :dim,
|
|
46
|
+
(0.2...0.4) => :peripheral,
|
|
47
|
+
(..0.2) => :unconscious
|
|
48
|
+
}.freeze
|
|
49
|
+
|
|
50
|
+
# Human-readable attention-state labels
|
|
51
|
+
ATTENTION_STATE_LABELS = {
|
|
52
|
+
hyperfocused: 'deeply engaged',
|
|
53
|
+
focused: 'actively attending',
|
|
54
|
+
normal: 'casually aware',
|
|
55
|
+
drifting: 'attention waning',
|
|
56
|
+
distracted: 'attention scattered'
|
|
57
|
+
}.freeze
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module AttentionSchema
|
|
6
|
+
module Helpers
|
|
7
|
+
class SchemaItem
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
attr_reader :target, :domain, :reason, :source, :created_at
|
|
11
|
+
attr_accessor :awareness_level
|
|
12
|
+
|
|
13
|
+
def initialize(target:, domain:, reason:, source:, awareness_level: DEFAULT_AWARENESS)
|
|
14
|
+
@target = target
|
|
15
|
+
@domain = domain
|
|
16
|
+
@reason = reason
|
|
17
|
+
@source = source
|
|
18
|
+
@awareness_level = awareness_level.clamp(0.0, 1.0)
|
|
19
|
+
@created_at = Time.now.utc
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Duration in seconds since this item entered the schema
|
|
23
|
+
def duration
|
|
24
|
+
Time.now.utc - @created_at
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Apply a one-time boost (re-focus event)
|
|
28
|
+
def boost
|
|
29
|
+
@awareness_level = [@awareness_level + AWARENESS_BOOST, 1.0].min
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Apply per-tick decay
|
|
33
|
+
def decay
|
|
34
|
+
@awareness_level = [@awareness_level - AWARENESS_DECAY, AWARENESS_FLOOR].max
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# True when awareness has dropped to the pruning floor
|
|
38
|
+
def faded?
|
|
39
|
+
@awareness_level <= AWARENESS_FLOOR
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Symbolic label describing current awareness intensity
|
|
43
|
+
def label
|
|
44
|
+
AWARENESS_LABELS.each { |range, lbl| return lbl if range.cover?(@awareness_level) }
|
|
45
|
+
:unconscious
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def to_h
|
|
49
|
+
{
|
|
50
|
+
target: @target,
|
|
51
|
+
domain: @domain,
|
|
52
|
+
awareness_level: @awareness_level.round(4),
|
|
53
|
+
label: label,
|
|
54
|
+
reason: @reason,
|
|
55
|
+
source: @source,
|
|
56
|
+
duration: duration.round(2),
|
|
57
|
+
created_at: @created_at
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module AttentionSchema
|
|
6
|
+
module Runners
|
|
7
|
+
module AttentionSchema
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
# Focus attention on a target — adds or boosts it in the schema
|
|
12
|
+
def focus_on(target:, domain:, reason:, source: :external, **)
|
|
13
|
+
Legion::Logging.debug "[attention_schema] focus_on: target=#{target} domain=#{domain} source=#{source}"
|
|
14
|
+
item = schema_model.focus_on(target: target, domain: domain, reason: reason, source: source)
|
|
15
|
+
{
|
|
16
|
+
success: true,
|
|
17
|
+
target: item.target,
|
|
18
|
+
domain: item.domain,
|
|
19
|
+
awareness_level: item.awareness_level.round(4),
|
|
20
|
+
label: item.label,
|
|
21
|
+
schema_size: schema_model.schema_size
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Remove a target from the attention schema
|
|
26
|
+
def defocus(target:, **)
|
|
27
|
+
Legion::Logging.debug "[attention_schema] defocus: target=#{target}"
|
|
28
|
+
removed = schema_model.defocus(target: target)
|
|
29
|
+
{ success: true, target: target, removed: removed, schema_size: schema_model.schema_size }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Core AST query: "am I aware of X?"
|
|
33
|
+
def am_i_aware_of(target:, **)
|
|
34
|
+
result = schema_model.am_i_aware_of(target: target)
|
|
35
|
+
Legion::Logging.debug "[attention_schema] am_i_aware_of: target=#{target} aware=#{result[:aware]} level=#{result[:awareness_level]}"
|
|
36
|
+
result.merge(success: true)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Generate a natural-language attention awareness report
|
|
40
|
+
def report_awareness(**)
|
|
41
|
+
report = schema_model.report_awareness
|
|
42
|
+
Legion::Logging.debug "[attention_schema] report_awareness: state=#{report[:state]} items=#{report[:items].size}"
|
|
43
|
+
report.merge(success: true)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Return the current qualitative attention state symbol
|
|
47
|
+
def attention_state(**)
|
|
48
|
+
state = schema_model.attention_state
|
|
49
|
+
label = Helpers::Constants::ATTENTION_STATE_LABELS[state]
|
|
50
|
+
Legion::Logging.debug "[attention_schema] attention_state: state=#{state}"
|
|
51
|
+
{ success: true, state: state, label: label }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Model another agent's attention (social attention modeling)
|
|
55
|
+
def model_other_attention(agent_id:, target:, awareness:, **)
|
|
56
|
+
Legion::Logging.debug "[attention_schema] model_other: agent=#{agent_id} target=#{target} awareness=#{awareness}"
|
|
57
|
+
schema_model.model_other_attention(agent_id: agent_id, target: target, awareness: awareness.to_f)
|
|
58
|
+
{ success: true, agent_id: agent_id, target: target, awareness: awareness.to_f.clamp(0.0, 1.0).round(4) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Query what another agent is modeled as attending to
|
|
62
|
+
def query_other_attention(agent_id:, **)
|
|
63
|
+
model = schema_model.query_other_attention(agent_id: agent_id)
|
|
64
|
+
Legion::Logging.debug "[attention_schema] query_other: agent=#{agent_id} found=#{!model.nil?}"
|
|
65
|
+
{ success: true, agent_id: agent_id, model: model }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Run meta-attention check: detect drifting, hyper-focus, etc.
|
|
69
|
+
def meta_check(**)
|
|
70
|
+
result = schema_model.meta_check
|
|
71
|
+
Legion::Logging.debug "[attention_schema] meta_check: state=#{result[:state]} signals=#{result[:signals]}"
|
|
72
|
+
result.merge(success: true)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Record whether the schema accurately predicted actual attention (accuracy feedback)
|
|
76
|
+
def update_meta_accuracy(was_correct:, **)
|
|
77
|
+
schema_model.update_meta_accuracy(was_correct: was_correct)
|
|
78
|
+
accuracy = schema_model.meta_accuracy
|
|
79
|
+
Legion::Logging.debug "[attention_schema] meta_accuracy: was_correct=#{was_correct} accuracy=#{accuracy.round(3)}"
|
|
80
|
+
{ success: true, was_correct: was_correct, meta_accuracy: accuracy.round(4) }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Tick decay: decay all schema items and prune faded ones
|
|
84
|
+
def decay_schema(**)
|
|
85
|
+
before = schema_model.schema_size
|
|
86
|
+
schema_model.decay_all
|
|
87
|
+
after = schema_model.schema_size
|
|
88
|
+
Legion::Logging.debug "[attention_schema] decay: before=#{before} after=#{after} pruned=#{before - after}"
|
|
89
|
+
{
|
|
90
|
+
success: true,
|
|
91
|
+
before: before,
|
|
92
|
+
after: after,
|
|
93
|
+
pruned: before - after,
|
|
94
|
+
state: schema_model.attention_state
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Return full schema stats snapshot
|
|
99
|
+
def schema_stats(**)
|
|
100
|
+
Legion::Logging.debug "[attention_schema] stats: size=#{schema_model.schema_size}"
|
|
101
|
+
{ success: true, stats: schema_model.to_h }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def schema_model
|
|
107
|
+
@schema_model ||= Helpers::AttentionSchemaModel.new
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/attention_schema/version'
|
|
4
|
+
require 'legion/extensions/attention_schema/helpers/constants'
|
|
5
|
+
require 'legion/extensions/attention_schema/helpers/schema_item'
|
|
6
|
+
require 'legion/extensions/attention_schema/helpers/attention_schema_model'
|
|
7
|
+
require 'legion/extensions/attention_schema/runners/attention_schema'
|
|
8
|
+
require 'legion/extensions/attention_schema/client'
|
|
9
|
+
|
|
10
|
+
module Legion
|
|
11
|
+
module Extensions
|
|
12
|
+
module AttentionSchema
|
|
13
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::AttentionSchema::Client do
|
|
4
|
+
subject(:client) { described_class.new }
|
|
5
|
+
|
|
6
|
+
it 'includes Runners::AttentionSchema' do
|
|
7
|
+
expect(described_class.ancestors).to include(Legion::Extensions::AttentionSchema::Runners::AttentionSchema)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it 'supports full attention lifecycle' do
|
|
11
|
+
# Focus on items
|
|
12
|
+
client.focus_on(target: :code_review, domain: :work, reason: 'PR needs review', source: :external)
|
|
13
|
+
client.focus_on(target: :test_results, domain: :ci, reason: 'tests running', source: :internal)
|
|
14
|
+
|
|
15
|
+
# Query awareness
|
|
16
|
+
aware = client.am_i_aware_of(target: :code_review)
|
|
17
|
+
expect(aware[:aware]).to be true
|
|
18
|
+
|
|
19
|
+
# Report
|
|
20
|
+
report = client.report_awareness
|
|
21
|
+
expect(report[:items].size).to eq(2)
|
|
22
|
+
|
|
23
|
+
# Model another agent
|
|
24
|
+
client.model_other_attention(agent_id: :validator, target: :code_review, awareness: 0.9)
|
|
25
|
+
other = client.query_other_attention(agent_id: :validator)
|
|
26
|
+
expect(other[:model]).not_to be_nil
|
|
27
|
+
|
|
28
|
+
# Meta-check
|
|
29
|
+
meta = client.meta_check
|
|
30
|
+
expect(meta[:schema_size]).to eq(2)
|
|
31
|
+
|
|
32
|
+
# Meta-accuracy feedback
|
|
33
|
+
client.update_meta_accuracy(was_correct: true)
|
|
34
|
+
|
|
35
|
+
# Decay
|
|
36
|
+
client.decay_schema
|
|
37
|
+
|
|
38
|
+
# Stats
|
|
39
|
+
stats = client.schema_stats
|
|
40
|
+
expect(stats[:success]).to be true
|
|
41
|
+
|
|
42
|
+
# Defocus
|
|
43
|
+
client.defocus(target: :test_results)
|
|
44
|
+
final = client.am_i_aware_of(target: :test_results)
|
|
45
|
+
expect(final[:aware]).to be false
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::AttentionSchema::Helpers::AttentionSchemaModel do
|
|
4
|
+
subject(:model) { described_class.new }
|
|
5
|
+
|
|
6
|
+
let(:constants) { Legion::Extensions::AttentionSchema::Helpers::Constants }
|
|
7
|
+
|
|
8
|
+
describe '#focus_on' do
|
|
9
|
+
it 'adds an item to the schema' do
|
|
10
|
+
item = model.focus_on(target: :task, domain: :work, reason: 'priority', source: :external)
|
|
11
|
+
expect(item.target).to eq('task')
|
|
12
|
+
expect(model.schema_size).to eq(1)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'boosts existing items on re-focus' do
|
|
16
|
+
model.focus_on(target: :task, domain: :work, reason: 'init', source: :external)
|
|
17
|
+
first_level = model.schema_items['task'].awareness_level
|
|
18
|
+
model.focus_on(target: :task, domain: :work, reason: 'again', source: :external)
|
|
19
|
+
expect(model.schema_items['task'].awareness_level).to be > first_level
|
|
20
|
+
expect(model.schema_size).to eq(1)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'prunes to capacity when full' do
|
|
24
|
+
constants::MAX_SCHEMA_ITEMS.times do |i|
|
|
25
|
+
model.focus_on(target: "item_#{i}", domain: :d, reason: :r, source: :s)
|
|
26
|
+
end
|
|
27
|
+
expect(model.schema_size).to eq(constants::MAX_SCHEMA_ITEMS)
|
|
28
|
+
model.focus_on(target: :overflow, domain: :d, reason: :r, source: :s)
|
|
29
|
+
expect(model.schema_size).to eq(constants::MAX_SCHEMA_ITEMS)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'records focus event in history' do
|
|
33
|
+
model.focus_on(target: :task, domain: :work, reason: 'test', source: :external)
|
|
34
|
+
expect(model.attention_history.size).to eq(1)
|
|
35
|
+
expect(model.attention_history.first[:event]).to eq(:focused)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'records refocused event on re-focus' do
|
|
39
|
+
model.focus_on(target: :task, domain: :work, reason: 'init', source: :external)
|
|
40
|
+
model.focus_on(target: :task, domain: :work, reason: 'again', source: :external)
|
|
41
|
+
expect(model.attention_history.last[:event]).to eq(:refocused)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe '#defocus' do
|
|
46
|
+
it 'removes an item from the schema' do
|
|
47
|
+
model.focus_on(target: :task, domain: :work, reason: 'r', source: :s)
|
|
48
|
+
removed = model.defocus(target: :task)
|
|
49
|
+
expect(removed).to be true
|
|
50
|
+
expect(model.schema_size).to eq(0)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'returns false for unknown target' do
|
|
54
|
+
expect(model.defocus(target: :missing)).to be false
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe '#am_i_aware_of' do
|
|
59
|
+
it 'returns aware: true for focused items' do
|
|
60
|
+
model.focus_on(target: :task, domain: :work, reason: 'r', source: :s)
|
|
61
|
+
result = model.am_i_aware_of(target: :task)
|
|
62
|
+
expect(result[:aware]).to be true
|
|
63
|
+
expect(result[:awareness_level]).to be > 0
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'returns aware: false for unknown targets' do
|
|
67
|
+
result = model.am_i_aware_of(target: :unknown)
|
|
68
|
+
expect(result[:aware]).to be false
|
|
69
|
+
expect(result[:awareness_level]).to eq(0.0)
|
|
70
|
+
expect(result[:label]).to eq(:unconscious)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
describe '#attention_state' do
|
|
75
|
+
it 'returns :distracted when empty' do
|
|
76
|
+
expect(model.attention_state).to eq(:distracted)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'returns :focused with a moderate item' do
|
|
80
|
+
model.focus_on(target: :task, domain: :work, reason: 'r', source: :s)
|
|
81
|
+
model.schema_items['task'].awareness_level = 0.7
|
|
82
|
+
expect(model.attention_state).to eq(:focused)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'returns :hyperfocused with high awareness' do
|
|
86
|
+
model.focus_on(target: :task, domain: :work, reason: 'r', source: :s)
|
|
87
|
+
model.schema_items['task'].awareness_level = 0.9
|
|
88
|
+
expect(model.attention_state).to eq(:hyperfocused)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it 'returns :drifting when average is low with few items' do
|
|
92
|
+
model.focus_on(target: :task, domain: :work, reason: 'r', source: :s)
|
|
93
|
+
model.schema_items['task'].awareness_level = 0.15
|
|
94
|
+
expect(model.attention_state).to eq(:drifting)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'returns :distracted with many low-awareness items' do
|
|
98
|
+
5.times { |i| model.focus_on(target: "t_#{i}", domain: :d, reason: :r, source: :s) }
|
|
99
|
+
model.schema_items.each_value { |v| v.awareness_level = 0.15 }
|
|
100
|
+
expect(model.attention_state).to eq(:distracted)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
describe '#report_awareness' do
|
|
105
|
+
it 'reports no items when empty' do
|
|
106
|
+
report = model.report_awareness
|
|
107
|
+
expect(report[:state]).to eq(:distracted)
|
|
108
|
+
expect(report[:items]).to be_empty
|
|
109
|
+
expect(report[:report]).to include('No active')
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'generates summary with items' do
|
|
113
|
+
model.focus_on(target: :task, domain: :work, reason: 'priority', source: :external)
|
|
114
|
+
report = model.report_awareness
|
|
115
|
+
expect(report[:report]).to include('task')
|
|
116
|
+
expect(report[:items].size).to eq(1)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it 'includes secondary items in report' do
|
|
120
|
+
model.focus_on(target: :a, domain: :work, reason: 'r', source: :s)
|
|
121
|
+
model.focus_on(target: :b, domain: :play, reason: 'r', source: :s)
|
|
122
|
+
report = model.report_awareness
|
|
123
|
+
expect(report[:items].size).to eq(2)
|
|
124
|
+
expect(report[:report]).to include('Also attending to')
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
describe '#model_other_attention' do
|
|
129
|
+
it 'records another agent attention model' do
|
|
130
|
+
model.model_other_attention(agent_id: :agent_a, target: :task, awareness: 0.8)
|
|
131
|
+
result = model.query_other_attention(agent_id: :agent_a)
|
|
132
|
+
expect(result[:target]).to eq('task')
|
|
133
|
+
expect(result[:awareness]).to eq(0.8)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it 'clamps awareness' do
|
|
137
|
+
model.model_other_attention(agent_id: :agent_a, target: :task, awareness: 1.5)
|
|
138
|
+
result = model.query_other_attention(agent_id: :agent_a)
|
|
139
|
+
expect(result[:awareness]).to eq(1.0)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it 'returns nil for unknown agent' do
|
|
143
|
+
expect(model.query_other_attention(agent_id: :unknown)).to be_nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it 'prunes oldest social model when full' do
|
|
147
|
+
constants::MAX_SOCIAL_MODELS.times do |i|
|
|
148
|
+
model.model_other_attention(agent_id: "agent_#{i}", target: :t, awareness: 0.5)
|
|
149
|
+
end
|
|
150
|
+
model.model_other_attention(agent_id: :overflow, target: :t, awareness: 0.5)
|
|
151
|
+
expect(model.social_models.size).to eq(constants::MAX_SOCIAL_MODELS)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
describe '#meta_check' do
|
|
156
|
+
it 'returns meta-attention state' do
|
|
157
|
+
result = model.meta_check
|
|
158
|
+
expect(result).to include(:state, :signals, :top_awareness, :avg_awareness, :schema_size, :meta_accuracy)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it 'includes :drifting signal when distracted' do
|
|
162
|
+
result = model.meta_check
|
|
163
|
+
expect(result[:signals]).to include(:drifting)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it 'includes :hyperfocus signal when hyperfocused' do
|
|
167
|
+
model.focus_on(target: :task, domain: :work, reason: 'r', source: :s)
|
|
168
|
+
model.schema_items['task'].awareness_level = 0.9
|
|
169
|
+
result = model.meta_check
|
|
170
|
+
expect(result[:signals]).to include(:hyperfocus)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it 'includes :normal when state is normal' do
|
|
174
|
+
model.focus_on(target: :task, domain: :work, reason: 'r', source: :s)
|
|
175
|
+
model.schema_items['task'].awareness_level = 0.5
|
|
176
|
+
result = model.meta_check
|
|
177
|
+
expect(result[:signals]).to include(:normal)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
describe '#update_meta_accuracy' do
|
|
182
|
+
it 'increases accuracy when correct' do
|
|
183
|
+
before = model.meta_accuracy
|
|
184
|
+
model.update_meta_accuracy(was_correct: true)
|
|
185
|
+
expect(model.meta_accuracy).to be > before
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it 'decreases accuracy when incorrect' do
|
|
189
|
+
before = model.meta_accuracy
|
|
190
|
+
model.update_meta_accuracy(was_correct: false)
|
|
191
|
+
expect(model.meta_accuracy).to be < before
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
describe '#decay_all' do
|
|
196
|
+
it 'decays all items' do
|
|
197
|
+
model.focus_on(target: :task, domain: :work, reason: 'r', source: :s)
|
|
198
|
+
before = model.schema_items['task'].awareness_level
|
|
199
|
+
model.decay_all
|
|
200
|
+
expect(model.schema_items['task']&.awareness_level || 0).to be < before
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it 'prunes faded items' do
|
|
204
|
+
model.focus_on(target: :task, domain: :work, reason: 'r', source: :s)
|
|
205
|
+
model.schema_items['task'].awareness_level = constants::AWARENESS_FLOOR + 0.01
|
|
206
|
+
model.decay_all
|
|
207
|
+
expect(model.schema_size).to eq(0)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
describe '#to_h' do
|
|
212
|
+
it 'returns stats hash' do
|
|
213
|
+
model.focus_on(target: :task, domain: :work, reason: 'r', source: :s)
|
|
214
|
+
h = model.to_h
|
|
215
|
+
expect(h).to include(:state, :state_label, :schema_size, :meta_accuracy, :top_awareness, :avg_awareness, :social_models, :items)
|
|
216
|
+
expect(h[:schema_size]).to eq(1)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::AttentionSchema::Helpers::SchemaItem do
|
|
4
|
+
subject(:item) do
|
|
5
|
+
described_class.new(target: 'task_queue', domain: :work, reason: 'priority', source: :external)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
let(:constants) { Legion::Extensions::AttentionSchema::Helpers::Constants }
|
|
9
|
+
|
|
10
|
+
describe '#initialize' do
|
|
11
|
+
it 'sets attributes' do
|
|
12
|
+
expect(item.target).to eq('task_queue')
|
|
13
|
+
expect(item.domain).to eq(:work)
|
|
14
|
+
expect(item.reason).to eq('priority')
|
|
15
|
+
expect(item.source).to eq(:external)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'uses default awareness level' do
|
|
19
|
+
expect(item.awareness_level).to eq(constants::DEFAULT_AWARENESS)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'clamps awareness to 0..1' do
|
|
23
|
+
high = described_class.new(target: :x, domain: :d, reason: :r, source: :s, awareness_level: 1.5)
|
|
24
|
+
low = described_class.new(target: :y, domain: :d, reason: :r, source: :s, awareness_level: -0.5)
|
|
25
|
+
expect(high.awareness_level).to eq(1.0)
|
|
26
|
+
expect(low.awareness_level).to eq(0.0)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'records created_at' do
|
|
30
|
+
expect(item.created_at).to be_a(Time)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe '#duration' do
|
|
35
|
+
it 'returns elapsed time' do
|
|
36
|
+
expect(item.duration).to be >= 0.0
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe '#boost' do
|
|
41
|
+
it 'increases awareness by AWARENESS_BOOST' do
|
|
42
|
+
before = item.awareness_level
|
|
43
|
+
item.boost
|
|
44
|
+
expect(item.awareness_level).to eq(before + constants::AWARENESS_BOOST)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'caps at 1.0' do
|
|
48
|
+
item.awareness_level = 0.95
|
|
49
|
+
item.boost
|
|
50
|
+
expect(item.awareness_level).to eq(1.0)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
describe '#decay' do
|
|
55
|
+
it 'decreases awareness by AWARENESS_DECAY' do
|
|
56
|
+
item.awareness_level = 0.5
|
|
57
|
+
item.decay
|
|
58
|
+
expect(item.awareness_level).to eq(0.5 - constants::AWARENESS_DECAY)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'does not drop below AWARENESS_FLOOR' do
|
|
62
|
+
item.awareness_level = constants::AWARENESS_FLOOR + 0.001
|
|
63
|
+
item.decay
|
|
64
|
+
expect(item.awareness_level).to eq(constants::AWARENESS_FLOOR)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe '#faded?' do
|
|
69
|
+
it 'returns true at floor' do
|
|
70
|
+
item.awareness_level = constants::AWARENESS_FLOOR
|
|
71
|
+
expect(item.faded?).to be true
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'returns false above floor' do
|
|
75
|
+
item.awareness_level = constants::AWARENESS_FLOOR + 0.01
|
|
76
|
+
expect(item.faded?).to be false
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
describe '#label' do
|
|
81
|
+
it 'returns :vivid for high awareness' do
|
|
82
|
+
item.awareness_level = 0.9
|
|
83
|
+
expect(item.label).to eq(:vivid)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'returns :unconscious for very low awareness' do
|
|
87
|
+
item.awareness_level = 0.1
|
|
88
|
+
expect(item.label).to eq(:unconscious)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it 'returns :clear for mid-high awareness' do
|
|
92
|
+
item.awareness_level = 0.7
|
|
93
|
+
expect(item.label).to eq(:clear)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'returns :dim for mid awareness' do
|
|
97
|
+
item.awareness_level = 0.5
|
|
98
|
+
expect(item.label).to eq(:dim)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'returns :peripheral for low awareness' do
|
|
102
|
+
item.awareness_level = 0.25
|
|
103
|
+
expect(item.label).to eq(:peripheral)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
describe '#to_h' do
|
|
108
|
+
it 'returns hash with all fields' do
|
|
109
|
+
h = item.to_h
|
|
110
|
+
expect(h).to include(:target, :domain, :awareness_level, :label, :reason, :source, :duration, :created_at)
|
|
111
|
+
expect(h[:target]).to eq('task_queue')
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::AttentionSchema::Runners::AttentionSchema do
|
|
4
|
+
let(:client) { Legion::Extensions::AttentionSchema::Client.new }
|
|
5
|
+
|
|
6
|
+
describe '#focus_on' do
|
|
7
|
+
it 'focuses on a target' do
|
|
8
|
+
result = client.focus_on(target: :task, domain: :work, reason: 'priority', source: :external)
|
|
9
|
+
expect(result[:success]).to be true
|
|
10
|
+
expect(result[:target]).to eq('task')
|
|
11
|
+
expect(result[:awareness_level]).to be > 0
|
|
12
|
+
expect(result[:label]).to be_a(Symbol)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'boosts on re-focus' do
|
|
16
|
+
first = client.focus_on(target: :task, domain: :work, reason: 'r', source: :s)
|
|
17
|
+
second = client.focus_on(target: :task, domain: :work, reason: 'r', source: :s)
|
|
18
|
+
expect(second[:awareness_level]).to be > first[:awareness_level]
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe '#defocus' do
|
|
23
|
+
it 'removes a target' do
|
|
24
|
+
client.focus_on(target: :task, domain: :work, reason: 'r', source: :s)
|
|
25
|
+
result = client.defocus(target: :task)
|
|
26
|
+
expect(result[:success]).to be true
|
|
27
|
+
expect(result[:removed]).to be true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'returns removed: false for unknown' do
|
|
31
|
+
result = client.defocus(target: :missing)
|
|
32
|
+
expect(result[:removed]).to be false
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe '#am_i_aware_of' do
|
|
37
|
+
it 'returns aware: true for focused target' do
|
|
38
|
+
client.focus_on(target: :task, domain: :work, reason: 'r', source: :s)
|
|
39
|
+
result = client.am_i_aware_of(target: :task)
|
|
40
|
+
expect(result[:success]).to be true
|
|
41
|
+
expect(result[:aware]).to be true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'returns aware: false for unknown target' do
|
|
45
|
+
result = client.am_i_aware_of(target: :unknown)
|
|
46
|
+
expect(result[:aware]).to be false
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe '#report_awareness' do
|
|
51
|
+
it 'returns awareness report' do
|
|
52
|
+
client.focus_on(target: :task, domain: :work, reason: 'priority', source: :external)
|
|
53
|
+
result = client.report_awareness
|
|
54
|
+
expect(result[:success]).to be true
|
|
55
|
+
expect(result[:report]).to be_a(String)
|
|
56
|
+
expect(result[:items]).to be_an(Array)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
describe '#attention_state' do
|
|
61
|
+
it 'returns current state' do
|
|
62
|
+
result = client.attention_state
|
|
63
|
+
expect(result[:success]).to be true
|
|
64
|
+
expect(result[:state]).to be_a(Symbol)
|
|
65
|
+
expect(result[:label]).to be_a(String)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
describe '#model_other_attention' do
|
|
70
|
+
it 'records other agent attention' do
|
|
71
|
+
result = client.model_other_attention(agent_id: :agent_a, target: :task, awareness: 0.8)
|
|
72
|
+
expect(result[:success]).to be true
|
|
73
|
+
expect(result[:agent_id]).to eq(:agent_a)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
describe '#query_other_attention' do
|
|
78
|
+
it 'queries modeled attention' do
|
|
79
|
+
client.model_other_attention(agent_id: :agent_a, target: :task, awareness: 0.8)
|
|
80
|
+
result = client.query_other_attention(agent_id: :agent_a)
|
|
81
|
+
expect(result[:success]).to be true
|
|
82
|
+
expect(result[:model]).not_to be_nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'returns nil model for unknown agent' do
|
|
86
|
+
result = client.query_other_attention(agent_id: :unknown)
|
|
87
|
+
expect(result[:model]).to be_nil
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
describe '#meta_check' do
|
|
92
|
+
it 'returns meta-attention signals' do
|
|
93
|
+
result = client.meta_check
|
|
94
|
+
expect(result[:success]).to be true
|
|
95
|
+
expect(result[:state]).to be_a(Symbol)
|
|
96
|
+
expect(result[:signals]).to be_an(Array)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
describe '#update_meta_accuracy' do
|
|
101
|
+
it 'updates meta-accuracy with feedback' do
|
|
102
|
+
result = client.update_meta_accuracy(was_correct: true)
|
|
103
|
+
expect(result[:success]).to be true
|
|
104
|
+
expect(result[:meta_accuracy]).to be > 0.5
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
describe '#decay_schema' do
|
|
109
|
+
it 'decays and reports' do
|
|
110
|
+
client.focus_on(target: :task, domain: :work, reason: 'r', source: :s)
|
|
111
|
+
result = client.decay_schema
|
|
112
|
+
expect(result[:success]).to be true
|
|
113
|
+
expect(result[:before]).to eq(1)
|
|
114
|
+
expect(result).to have_key(:after)
|
|
115
|
+
expect(result).to have_key(:pruned)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
describe '#schema_stats' do
|
|
120
|
+
it 'returns stats hash' do
|
|
121
|
+
result = client.schema_stats
|
|
122
|
+
expect(result[:success]).to be true
|
|
123
|
+
expect(result[:stats]).to include(:state, :schema_size, :meta_accuracy)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it 'reports zero schema_size when empty' do
|
|
127
|
+
result = client.schema_stats
|
|
128
|
+
expect(result[:stats][:schema_size]).to eq(0)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it 'includes items array in stats' do
|
|
132
|
+
client.focus_on(target: :task, domain: :work, reason: 'r', source: :s)
|
|
133
|
+
result = client.schema_stats
|
|
134
|
+
expect(result[:stats][:items]).to be_an(Array)
|
|
135
|
+
expect(result[:stats][:items].size).to eq(1)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
describe '#focus_on schema_size tracking' do
|
|
140
|
+
it 'tracks schema_size correctly through multiple targets' do
|
|
141
|
+
client.focus_on(target: :a, domain: :d, reason: 'r', source: :s)
|
|
142
|
+
r2 = client.focus_on(target: :b, domain: :d, reason: 'r', source: :s)
|
|
143
|
+
r3 = client.focus_on(target: :c, domain: :d, reason: 'r', source: :s)
|
|
144
|
+
expect(r2[:schema_size]).to eq(2)
|
|
145
|
+
expect(r3[:schema_size]).to eq(3)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
describe '#report_awareness empty state' do
|
|
150
|
+
it 'returns empty items when nothing focused' do
|
|
151
|
+
result = client.report_awareness
|
|
152
|
+
expect(result[:items]).to be_empty
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it 'includes state_label string' do
|
|
156
|
+
result = client.report_awareness
|
|
157
|
+
expect(result[:state_label]).to be_a(String)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
describe '#meta_check state labeling' do
|
|
162
|
+
it 'returns :distracted state with empty schema' do
|
|
163
|
+
result = client.meta_check
|
|
164
|
+
expect(result[:state]).to eq(:distracted)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it 'returns numeric top_awareness and avg_awareness' do
|
|
168
|
+
result = client.meta_check
|
|
169
|
+
expect(result[:top_awareness]).to be_a(Float)
|
|
170
|
+
expect(result[:avg_awareness]).to be_a(Float)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
describe '#update_meta_accuracy boundary behavior' do
|
|
175
|
+
it 'correct=false reduces meta_accuracy below initial' do
|
|
176
|
+
result = client.update_meta_accuracy(was_correct: false)
|
|
177
|
+
expect(result[:meta_accuracy]).to be < 0.5
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
it 'returns the was_correct value echoed back' do
|
|
181
|
+
result = client.update_meta_accuracy(was_correct: false)
|
|
182
|
+
expect(result[:was_correct]).to be false
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -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/attention_schema'
|
|
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,78 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-attention-schema
|
|
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: Graziano's Attention Schema Theory for brain-modeled agentic AI — the
|
|
27
|
+
agent maintains a simplified internal model of its own attention process, enabling
|
|
28
|
+
awareness attribution, social attention modeling, meta-attention monitoring, and
|
|
29
|
+
natural-language attention reports.
|
|
30
|
+
email:
|
|
31
|
+
- matthewdiverson@gmail.com
|
|
32
|
+
executables: []
|
|
33
|
+
extensions: []
|
|
34
|
+
extra_rdoc_files: []
|
|
35
|
+
files:
|
|
36
|
+
- Gemfile
|
|
37
|
+
- lex-attention-schema.gemspec
|
|
38
|
+
- lib/legion/extensions/attention_schema.rb
|
|
39
|
+
- lib/legion/extensions/attention_schema/actors/decay.rb
|
|
40
|
+
- lib/legion/extensions/attention_schema/client.rb
|
|
41
|
+
- lib/legion/extensions/attention_schema/helpers/attention_schema_model.rb
|
|
42
|
+
- lib/legion/extensions/attention_schema/helpers/constants.rb
|
|
43
|
+
- lib/legion/extensions/attention_schema/helpers/schema_item.rb
|
|
44
|
+
- lib/legion/extensions/attention_schema/runners/attention_schema.rb
|
|
45
|
+
- lib/legion/extensions/attention_schema/version.rb
|
|
46
|
+
- spec/legion/extensions/attention_schema/client_spec.rb
|
|
47
|
+
- spec/legion/extensions/attention_schema/helpers/attention_schema_model_spec.rb
|
|
48
|
+
- spec/legion/extensions/attention_schema/helpers/schema_item_spec.rb
|
|
49
|
+
- spec/legion/extensions/attention_schema/runners/attention_schema_spec.rb
|
|
50
|
+
- spec/spec_helper.rb
|
|
51
|
+
homepage: https://github.com/LegionIO/lex-attention-schema
|
|
52
|
+
licenses:
|
|
53
|
+
- MIT
|
|
54
|
+
metadata:
|
|
55
|
+
homepage_uri: https://github.com/LegionIO/lex-attention-schema
|
|
56
|
+
source_code_uri: https://github.com/LegionIO/lex-attention-schema
|
|
57
|
+
documentation_uri: https://github.com/LegionIO/lex-attention-schema
|
|
58
|
+
changelog_uri: https://github.com/LegionIO/lex-attention-schema
|
|
59
|
+
bug_tracker_uri: https://github.com/LegionIO/lex-attention-schema/issues
|
|
60
|
+
rubygems_mfa_required: 'true'
|
|
61
|
+
rdoc_options: []
|
|
62
|
+
require_paths:
|
|
63
|
+
- lib
|
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3.4'
|
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '0'
|
|
74
|
+
requirements: []
|
|
75
|
+
rubygems_version: 3.6.9
|
|
76
|
+
specification_version: 4
|
|
77
|
+
summary: LEX Attention Schema
|
|
78
|
+
test_files: []
|