lex-agentic-social 0.1.6 → 0.1.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1e4e4cf74f2655992c34c4945a8da0de92c597fc5371ff6fa595e583a4cdd88a
4
- data.tar.gz: b0696e9bc1f7b990fb8e3a153fd22755edf34e18a6fdce7ea7efe23eccaa5afb
3
+ metadata.gz: 9b5652d5875cf5aae7e51adf8b8b09f0e664a719144a7575ac9eb2f4bd7b7b56
4
+ data.tar.gz: 62101c073d3970708e278b2e7afd1dbbe3efc582eb03246bf19f975ad1999bd4
5
5
  SHA512:
6
- metadata.gz: 362432beb980f6e62f4097db1c37f444929ce2edd84439daee258de6e15dae346b5c33b09e3bfab19b2ca5d38ecd75b8f65269099247586750c329e05f5e7728
7
- data.tar.gz: 1309465c2e0655fe62b68b3bfb58cf4f07eb9bbd808215927eb64c240dd4a16575117eb1e3cf6c757d7cd40e586bf3416ce4ddc3ba7feb040d8b2589f6f34c9a
6
+ metadata.gz: 5e210bfd1a546461a34290e84e49a893cbb792c1d304b2087c759d2345a0cc30ac37c3f15760024ab19fdf83106f7cdefb87fa83c025100fd36852931f60c82f
7
+ data.tar.gz: 7a49e748bfc08d1c918c494f16d8ec4f579e63b3ec97120b532d168ffc9865e1748c9113025c0dfe7c9505c0f1aa3830624299e16926bc1b60be52187a71b0a5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.8] - 2026-03-31
4
+
5
+ ### Added
6
+ - Attachment sub-module for Phase C bond modeling
7
+ - `AttachmentModel`: per-agent attachment strength, style, and stage tracking with EMA updates and no-regression stage transitions
8
+ - `AttachmentStore`: in-memory store with dirty tracking and Apollo Local persistence (`to_apollo_entries`, `from_apollo`)
9
+ - `Attachment` runner: `update_attachment` (tick integration), `reflect_on_bonds` (dream cycle orchestrator reading cross-module bond data), `attachment_stats`
10
+
11
+ ## [0.1.7] - 2026-03-31
12
+
13
+ ### Changed
14
+ - Migrate `TrustMap` persistence from Data::Local SQLite to Apollo Local (`to_apollo_entries`, `from_apollo`)
15
+ - Add dirty tracking (`dirty?`, `mark_clean!`) to `TrustMap` matching SocialGraph/MentalStateTracker pattern
16
+ - Tag schema: `['trust', 'trust_entry', '<agent_id>', '<domain>']` with optional `'partner'` tag via BondRegistry
17
+ - Remove Data::Local migration registration from trust entry point (migration file retained for existing installs)
18
+ - Add one-time `scripts/migrate_trust_to_apollo.rb` for legacy SQLite data migration
19
+
3
20
  ## [0.1.6] - 2026-03-31
4
21
 
5
22
  ### Added
data/Gemfile CHANGED
@@ -3,3 +3,5 @@
3
3
  source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
+
7
+ gem 'rubocop-legion'
@@ -33,7 +33,7 @@ Gem::Specification.new do |spec|
33
33
  spec.add_dependency 'legion-transport', '>= 1.3.9'
34
34
 
35
35
  spec.add_development_dependency 'rspec', '~> 3.13'
36
- spec.add_development_dependency 'rubocop', '~> 1.60'
37
- spec.add_development_dependency 'rubocop-legion', '~> 0.1'
38
- spec.add_development_dependency 'rubocop-rspec', '~> 2.26'
36
+ spec.add_development_dependency 'rubocop'
37
+ spec.add_development_dependency 'rubocop-legion'
38
+ spec.add_development_dependency 'rubocop-rspec'
39
39
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Agentic
6
+ module Social
7
+ module Attachment
8
+ class Client
9
+ include Runners::Attachment
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Agentic
6
+ module Social
7
+ module Attachment
8
+ module Helpers
9
+ class AttachmentModel
10
+ attr_reader :agent_id, :attachment_strength, :attachment_style,
11
+ :bond_stage, :separation_tolerance, :interaction_count
12
+
13
+ def initialize(agent_id:)
14
+ @agent_id = agent_id
15
+ @attachment_strength = 0.0
16
+ @attachment_style = :secure
17
+ @bond_stage = :initial
18
+ @separation_tolerance = Constants::BASE_SEPARATION_TOLERANCE
19
+ @interaction_count = 0
20
+ end
21
+
22
+ def update_from_signals(opts = {})
23
+ frequency_score = opts.fetch(:frequency_score, 0.0)
24
+ reciprocity_score = opts.fetch(:reciprocity_score, 0.0)
25
+ prediction_accuracy = opts.fetch(:prediction_accuracy, 0.0)
26
+ direct_address_ratio = opts.fetch(:direct_address_ratio, 0.0)
27
+ channel_consistency = opts.fetch(:channel_consistency, 0.0)
28
+
29
+ raw = (frequency_score * Constants::FREQUENCY_WEIGHT) +
30
+ (reciprocity_score * Constants::RECIPROCITY_WEIGHT) +
31
+ (prediction_accuracy * Constants::PREDICTION_ACCURACY_WEIGHT) +
32
+ (direct_address_ratio * Constants::DIRECT_ADDRESS_WEIGHT) +
33
+ (channel_consistency * Constants::CHANNEL_CONSISTENCY_WEIGHT)
34
+
35
+ @attachment_strength = if @interaction_count.zero?
36
+ raw.clamp(0.0, 1.0)
37
+ else
38
+ alpha = Constants::STRENGTH_ALPHA
39
+ ((alpha * raw) + ((1.0 - alpha) * @attachment_strength)).clamp(0.0, 1.0)
40
+ end
41
+ @interaction_count += 1
42
+ end
43
+
44
+ def update_stage!
45
+ new_stage = derive_stage
46
+ return if Constants::BOND_STAGES.index(new_stage) <= Constants::BOND_STAGES.index(@bond_stage)
47
+
48
+ @bond_stage = new_stage
49
+ @separation_tolerance = Constants::BASE_SEPARATION_TOLERANCE +
50
+ Constants::SEPARATION_TOLERANCE_GROWTH.fetch(@bond_stage, 0)
51
+ end
52
+
53
+ def derive_style!(opts = {})
54
+ frequency_variance = opts.fetch(:frequency_variance, 0.0)
55
+ reciprocity_imbalance = opts.fetch(:reciprocity_imbalance, 0.0)
56
+ frequency = opts.fetch(:frequency, 0.0)
57
+ direct_address_ratio = opts.fetch(:direct_address_ratio, 0.0)
58
+ thresholds = Constants::STYLE_THRESHOLDS
59
+ @attachment_style = if frequency_variance > thresholds[:anxious_frequency_variance] &&
60
+ reciprocity_imbalance > thresholds[:anxious_reciprocity_imbalance]
61
+ :anxious
62
+ elsif frequency < thresholds[:avoidant_frequency] &&
63
+ direct_address_ratio < thresholds[:avoidant_direct_address]
64
+ :avoidant
65
+ else
66
+ :secure
67
+ end
68
+ end
69
+
70
+ def to_h
71
+ { agent_id: @agent_id, attachment_strength: @attachment_strength,
72
+ attachment_style: @attachment_style, bond_stage: @bond_stage,
73
+ separation_tolerance: @separation_tolerance, interaction_count: @interaction_count }
74
+ end
75
+
76
+ def self.from_h(hash)
77
+ model = new(agent_id: hash[:agent_id])
78
+ model.instance_variable_set(:@attachment_strength, hash[:attachment_strength].to_f)
79
+ model.instance_variable_set(:@attachment_style, hash[:attachment_style]&.to_sym || :secure)
80
+ model.instance_variable_set(:@bond_stage, hash[:bond_stage]&.to_sym || :initial)
81
+ model.instance_variable_set(:@separation_tolerance, hash[:separation_tolerance]&.to_i || 3)
82
+ model.instance_variable_set(:@interaction_count, hash[:interaction_count].to_i)
83
+ model
84
+ end
85
+
86
+ private
87
+
88
+ def derive_stage
89
+ Constants::STAGE_THRESHOLDS.each_key.reverse_each do |stage|
90
+ threshold = Constants::STAGE_THRESHOLDS[stage]
91
+ return stage if @interaction_count >= threshold[:interactions] &&
92
+ @attachment_strength >= threshold[:strength]
93
+ end
94
+ :initial
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Agentic
6
+ module Social
7
+ module Attachment
8
+ module Helpers
9
+ class AttachmentStore
10
+ def initialize
11
+ @models = {}
12
+ @dirty = false
13
+ end
14
+
15
+ def get(agent_id)
16
+ @models[agent_id.to_s]
17
+ end
18
+
19
+ def get_or_create(agent_id)
20
+ key = agent_id.to_s
21
+ @models[key] ||= begin
22
+ @dirty = true
23
+ AttachmentModel.new(agent_id: key)
24
+ end
25
+ end
26
+
27
+ def all_models
28
+ @models.values
29
+ end
30
+
31
+ def dirty?
32
+ @dirty
33
+ end
34
+
35
+ def mark_clean!
36
+ @dirty = false
37
+ self
38
+ end
39
+
40
+ def to_apollo_entries
41
+ @models.map do |agent_id, model|
42
+ tags = Constants::TAG_PREFIX.dup + [agent_id]
43
+ tags << 'partner' if partner?(agent_id)
44
+ content = serialize(model.to_h)
45
+ { content: content, tags: tags }
46
+ end
47
+ end
48
+
49
+ def from_apollo(store:)
50
+ result = store.query(text: 'bond attachment', tags: %w[bond attachment])
51
+ return false unless result[:success] && result[:results]&.any?
52
+
53
+ result[:results].each do |entry|
54
+ parsed = deserialize(entry[:content])
55
+ next unless parsed && parsed[:agent_id]
56
+
57
+ @models[parsed[:agent_id].to_s] = AttachmentModel.from_h(parsed)
58
+ end
59
+ true
60
+ rescue StandardError => e
61
+ warn "[attachment_store] from_apollo error: #{e.message}"
62
+ false
63
+ end
64
+
65
+ private
66
+
67
+ def partner?(agent_id)
68
+ defined?(Legion::Gaia::BondRegistry) && Legion::Gaia::BondRegistry.partner?(agent_id)
69
+ end
70
+
71
+ def serialize(hash)
72
+ defined?(Legion::JSON) ? Legion::JSON.dump(hash) : ::JSON.dump(hash)
73
+ end
74
+
75
+ def deserialize(content)
76
+ parsed = defined?(Legion::JSON) ? Legion::JSON.parse(content) : ::JSON.parse(content, symbolize_names: true)
77
+ parsed.transform_keys(&:to_sym)
78
+ rescue StandardError => _e
79
+ nil
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Agentic
6
+ module Social
7
+ module Attachment
8
+ module Helpers
9
+ module Constants
10
+ # Strength computation weights (must sum to 1.0)
11
+ FREQUENCY_WEIGHT = 0.3
12
+ RECIPROCITY_WEIGHT = 0.25
13
+ PREDICTION_ACCURACY_WEIGHT = 0.2
14
+ DIRECT_ADDRESS_WEIGHT = 0.15
15
+ CHANNEL_CONSISTENCY_WEIGHT = 0.1
16
+
17
+ # Bond lifecycle stages
18
+ BOND_STAGES = %i[initial forming established deep].freeze
19
+
20
+ # Attachment style classifications
21
+ ATTACHMENT_STYLES = %i[secure anxious avoidant].freeze
22
+
23
+ # Stage progression thresholds
24
+ STAGE_THRESHOLDS = {
25
+ forming: { interactions: 10, strength: 0.3 },
26
+ established: { interactions: 50, strength: 0.5 },
27
+ deep: { interactions: 200, strength: 0.7 }
28
+ }.freeze
29
+
30
+ # Separation tolerance (consecutive prediction misses before anxiety signal)
31
+ BASE_SEPARATION_TOLERANCE = 3
32
+ SEPARATION_TOLERANCE_GROWTH = {
33
+ initial: 0, forming: 1, established: 2, deep: 4
34
+ }.freeze
35
+
36
+ # EMA alpha for strength updates
37
+ STRENGTH_ALPHA = 0.15
38
+
39
+ # Style derivation thresholds
40
+ STYLE_THRESHOLDS = {
41
+ anxious_frequency_variance: 0.4,
42
+ anxious_reciprocity_imbalance: 0.3,
43
+ avoidant_frequency: 0.2,
44
+ avoidant_direct_address: 0.15
45
+ }.freeze
46
+
47
+ # Apollo Local tag prefix
48
+ TAG_PREFIX = %w[bond attachment].freeze
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Agentic
6
+ module Social
7
+ module Attachment
8
+ module Runners
9
+ module Attachment
10
+ include Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers) &&
11
+ Legion::Extensions::Helpers.const_defined?(:Lex, false)
12
+
13
+ def update_attachment(tick_results: {}, human_observations: [], **)
14
+ agents = collect_agent_ids(tick_results, human_observations)
15
+ return { agents_updated: 0 } if agents.empty?
16
+
17
+ agents.each do |agent_id|
18
+ model = attachment_store.get_or_create(agent_id)
19
+ signals = extract_signals(agent_id, tick_results, human_observations)
20
+ model.update_from_signals(signals)
21
+ model.update_stage!
22
+ model.derive_style!(extract_style_signals(agent_id, human_observations))
23
+ end
24
+
25
+ { agents_updated: agents.size, models: agents.map { |id| attachment_store.get(id)&.to_h } }
26
+ end
27
+
28
+ def reflect_on_bonds(_tick_results: {}, _bond_summary: {}, **)
29
+ store = apollo_local_store
30
+ return { success: false, error: :no_store } unless store
31
+
32
+ partner_id = resolve_partner_id
33
+ model = attachment_store.get(partner_id) if partner_id
34
+ comm_patterns = read_communication_patterns(store, partner_id)
35
+ arc_state = read_relationship_arc(store, partner_id)
36
+ health = compute_relationship_health(model, comm_patterns, arc_state)
37
+
38
+ {
39
+ bonds_reflected: attachment_store.all_models.size,
40
+ partner_bond: if model
41
+ {
42
+ stage: model.bond_stage,
43
+ strength: model.attachment_strength,
44
+ style: model.attachment_style,
45
+ health: health,
46
+ milestones_today: arc_state[:milestones_today] || [],
47
+ narrative: nil
48
+ }
49
+ end
50
+ }
51
+ rescue StandardError => e
52
+ { success: false, error: e.message }
53
+ end
54
+
55
+ def attachment_stats(**)
56
+ partner_id = resolve_partner_id
57
+ partner_model = attachment_store.get(partner_id) if partner_id
58
+ {
59
+ bonds_tracked: attachment_store.all_models.size,
60
+ partner_bond: partner_model&.to_h
61
+ }
62
+ end
63
+
64
+ private
65
+
66
+ def attachment_store
67
+ @attachment_store ||= Helpers::AttachmentStore.new
68
+ end
69
+
70
+ def collect_agent_ids(tick_results, human_observations)
71
+ ids = Set.new
72
+ (tick_results.dig(:social_cognition, :reputation_updates) || []).each do |u|
73
+ ids << u[:agent_id].to_s if u[:agent_id]
74
+ end
75
+ human_observations.each { |o| ids << o[:agent_id].to_s if o[:agent_id] }
76
+ ids.to_a
77
+ end
78
+
79
+ def extract_signals(agent_id, tick_results, human_observations)
80
+ reputation = (tick_results.dig(:social_cognition, :reputation_updates) || [])
81
+ .find { |u| u[:agent_id].to_s == agent_id }
82
+ prediction = tick_results.dig(:theory_of_mind, :prediction_accuracy) || {}
83
+ obs = human_observations.select { |o| o[:agent_id].to_s == agent_id }
84
+
85
+ direct_count = obs.count { |o| o[:direct_address] }
86
+ channels = obs.filter_map { |o| o[:channel] }.uniq
87
+
88
+ {
89
+ frequency_score: obs.size.clamp(0, 10) / 10.0,
90
+ reciprocity_score: (reputation&.dig(:composite) || 0.0).clamp(0.0, 1.0),
91
+ prediction_accuracy: (prediction[agent_id] || 0.0).clamp(0.0, 1.0),
92
+ direct_address_ratio: obs.empty? ? 0.0 : (direct_count.to_f / obs.size).clamp(0.0, 1.0),
93
+ channel_consistency: channels.size <= 1 ? 1.0 : (1.0 / channels.size).clamp(0.0, 1.0)
94
+ }
95
+ end
96
+
97
+ def extract_style_signals(agent_id, human_observations)
98
+ obs = human_observations.select { |o| o[:agent_id].to_s == agent_id }
99
+ direct_count = obs.count { |o| o[:direct_address] }
100
+ {
101
+ frequency_variance: 0.0,
102
+ reciprocity_imbalance: 0.0,
103
+ frequency: obs.size.clamp(0, 10) / 10.0,
104
+ direct_address_ratio: obs.empty? ? 0.0 : direct_count.to_f / obs.size
105
+ }
106
+ end
107
+
108
+ def resolve_partner_id
109
+ if defined?(Legion::Gaia::BondRegistry)
110
+ bond = Legion::Gaia::BondRegistry.all_bonds.find { |b| b[:role] == :partner }
111
+ return bond&.dig(:identity)&.to_s
112
+ end
113
+
114
+ strongest = attachment_store.all_models.max_by(&:attachment_strength)
115
+ strongest&.agent_id
116
+ end
117
+
118
+ def apollo_local_store
119
+ return nil unless defined?(Legion::Apollo::Local) && Legion::Apollo::Local.started?
120
+
121
+ Legion::Apollo::Local
122
+ end
123
+
124
+ def read_communication_patterns(store, partner_id)
125
+ return {} unless partner_id
126
+
127
+ result = store.query(text: 'communication_pattern',
128
+ tags: ['bond', 'communication_pattern', partner_id])
129
+ return {} unless result[:success] && result[:results]&.any?
130
+
131
+ deserialize(result[:results].first[:content]) || {}
132
+ rescue StandardError => _e
133
+ {}
134
+ end
135
+
136
+ def read_relationship_arc(store, partner_id)
137
+ return {} unless partner_id
138
+
139
+ result = store.query(text: 'relationship_arc',
140
+ tags: ['bond', 'relationship_arc', partner_id])
141
+ return {} unless result[:success] && result[:results]&.any?
142
+
143
+ deserialize(result[:results].first[:content]) || {}
144
+ rescue StandardError => _e
145
+ {}
146
+ end
147
+
148
+ def compute_relationship_health(model, comm_patterns, arc_state)
149
+ return 0.0 unless model
150
+
151
+ strength_component = model.attachment_strength * 0.4
152
+ reciprocity_component = (arc_state[:reciprocity_balance] || 0.5) * 0.3
153
+ consistency_component = (comm_patterns[:consistency] || 0.5) * 0.3
154
+ (strength_component + reciprocity_component + consistency_component).clamp(0.0, 1.0)
155
+ end
156
+
157
+ def deserialize(content)
158
+ parsed = defined?(Legion::JSON) ? Legion::JSON.parse(content) : ::JSON.parse(content, symbolize_names: true)
159
+ parsed.is_a?(Hash) ? parsed.transform_keys(&:to_sym) : {}
160
+ rescue StandardError => _e
161
+ {}
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Agentic
6
+ module Social
7
+ module Attachment
8
+ VERSION = '0.1.0'
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'attachment/version'
4
+ require_relative 'attachment/helpers/constants'
5
+ require_relative 'attachment/helpers/attachment_model'
6
+ require_relative 'attachment/helpers/attachment_store'
7
+ require_relative 'attachment/runners/attachment'
8
+ require_relative 'attachment/client'