lex-narrative-self 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 336a5c4eb91fca23131bab8a2566fe8398bba7541dcc68e1c437c6a1a8c9067d
4
+ data.tar.gz: c43bcd16be3bca9f7a320b40e62e2dcb3aa08b5a0953685674924bbdb0269a4f
5
+ SHA512:
6
+ metadata.gz: cf610636d9fce5865681b7b7dc143926f99bcb91b2b26d8526e2f23cfdbe0bade5ce1c746eaa8667e417f7c1de1dabc8f14dc9ec24ae86c72accbf6d1376ca33
7
+ data.tar.gz: a61cad3fb3f040028f902dec6c6f99a4a9291caadef117277f29421872403b2ca909e87ef938f5668facaa427daf9667dcca9ea1123681bd8a5c7d9269bbf0c9
data/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # lex-narrative-self
2
+
3
+ Autobiographical narrative and self-concept for the LegionIO cognitive architecture. Models the agent's personal history as episodes, threads, and an evolving identity.
4
+
5
+ ## What It Does
6
+
7
+ Maintains an autobiography of personal episodes — each with a significance score, emotional valence, domain, and tags. Episodes are grouped into narrative threads by thematic overlap. A self-concept emerges from the statistical pattern of episode types over time via exponential moving average. Older low-significance episodes fade naturally.
8
+
9
+ ## Usage
10
+
11
+ ```ruby
12
+ client = Legion::Extensions::NarrativeSelf::Client.new
13
+
14
+ # Record an experience
15
+ client.record_episode(
16
+ description: 'Successfully debugged a complex race condition',
17
+ episode_type: :achievement,
18
+ domain: :engineering,
19
+ significance: 0.8,
20
+ emotional_valence: 0.7,
21
+ tags: ['debugging', 'concurrency']
22
+ )
23
+
24
+ # Create a narrative thread to track a recurring theme
25
+ client.create_thread(theme: 'debugging', domain: :engineering)
26
+
27
+ # Retrieve recent and significant episodes
28
+ client.recent_episodes(count: 5)
29
+ client.significant_episodes(min_significance: 0.7)
30
+
31
+ # Self-concept summary
32
+ summary = client.self_summary
33
+ # => {
34
+ # total_episodes: 42,
35
+ # dominant_types: [:achievement, :discovery, :insight],
36
+ # dominant_domains: [:engineering, :coordination],
37
+ # active_threads: ['debugging', 'planning'],
38
+ # self_concept: { achievement: 0.72, discovery: 0.61, ... },
39
+ # narrative_richness: 0.64
40
+ # }
41
+
42
+ # Tick decay (call each processing cycle)
43
+ client.update_narrative_self
44
+ ```
45
+
46
+ ## Episode Types
47
+
48
+ `:achievement`, `:failure`, `:discovery`, `:connection`, `:conflict`, `:resolution`, `:insight`, `:surprise`, `:decision`, `:transition`, `:reflection`
49
+
50
+ ## Significance Labels
51
+
52
+ | Range | Label |
53
+ |---|---|
54
+ | 0.8+ | `:pivotal` |
55
+ | 0.6–0.8 | `:important` |
56
+ | 0.3–0.6 | `:routine` |
57
+ | < 0.3 | `:minor` |
58
+
59
+ ## Development
60
+
61
+ ```bash
62
+ bundle install
63
+ bundle exec rspec
64
+ bundle exec rubocop
65
+ ```
66
+
67
+ ## License
68
+
69
+ MIT
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/narrative_self/helpers/constants'
4
+ require 'legion/extensions/narrative_self/helpers/episode'
5
+ require 'legion/extensions/narrative_self/helpers/narrative_thread'
6
+ require 'legion/extensions/narrative_self/helpers/autobiography'
7
+ require 'legion/extensions/narrative_self/runners/narrative_self'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module NarrativeSelf
12
+ class Client
13
+ include Runners::NarrativeSelf
14
+
15
+ attr_reader :autobiography
16
+
17
+ def initialize(autobiography: nil, **)
18
+ @autobiography = autobiography || Helpers::Autobiography.new
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module NarrativeSelf
6
+ module Helpers
7
+ class Autobiography
8
+ include Constants
9
+
10
+ attr_reader :episodes, :threads, :self_concept
11
+
12
+ def initialize
13
+ @episodes = []
14
+ @threads = []
15
+ @self_concept = {}
16
+ end
17
+
18
+ def record_episode(description:, episode_type: :insight, domain: :general,
19
+ significance: nil, emotional_valence: 0.0, tags: [])
20
+ episode = Episode.new(
21
+ description: description,
22
+ episode_type: episode_type,
23
+ domain: domain,
24
+ significance: significance,
25
+ emotional_valence: emotional_valence,
26
+ tags: tags
27
+ )
28
+ @episodes << episode
29
+ auto_link_threads(episode)
30
+ update_self_concept(episode)
31
+ trim_episodes
32
+ episode
33
+ end
34
+
35
+ def find_episode(id)
36
+ @episodes.find { |e| e.id == id }
37
+ end
38
+
39
+ def recent_episodes(count = 10)
40
+ @episodes.last(count)
41
+ end
42
+
43
+ def significant_episodes(min_significance: 0.6)
44
+ @episodes.select { |e| e.significance >= min_significance }
45
+ .sort_by { |e| -e.significance }
46
+ end
47
+
48
+ def episodes_by_type(episode_type)
49
+ @episodes.select { |e| e.episode_type == episode_type }
50
+ end
51
+
52
+ def episodes_in_domain(domain)
53
+ @episodes.select { |e| e.domain == domain }
54
+ end
55
+
56
+ def create_thread(theme:, domain: :general)
57
+ thread = NarrativeThread.new(theme: theme, domain: domain)
58
+ @threads << thread
59
+ trim_threads
60
+ thread
61
+ end
62
+
63
+ def find_thread(id)
64
+ @threads.find { |t| t.id == id }
65
+ end
66
+
67
+ def find_threads_by_theme(theme)
68
+ @threads.select { |t| t.theme == theme }
69
+ end
70
+
71
+ def strongest_threads(count = 5)
72
+ @threads.sort_by { |t| -t.strength }.first(count)
73
+ end
74
+
75
+ def timeline(window: MAX_TIMELINE_WINDOW)
76
+ @episodes.last(window).map(&:to_h)
77
+ end
78
+
79
+ def self_summary
80
+ top_types = episode_type_distribution.first(3).map(&:first)
81
+ top_domains = domain_distribution.first(3).map(&:first)
82
+ top_threads = strongest_threads(3).map(&:theme)
83
+ {
84
+ total_episodes: @episodes.size,
85
+ dominant_types: top_types,
86
+ dominant_domains: top_domains,
87
+ active_threads: top_threads,
88
+ self_concept: @self_concept.dup,
89
+ pivotal_count: @episodes.count { |e| e.label == :pivotal },
90
+ narrative_richness: narrative_richness
91
+ }
92
+ end
93
+
94
+ def decay_all
95
+ @episodes.each(&:decay)
96
+ @episodes.reject!(&:faded?)
97
+ @threads.each(&:decay)
98
+ @threads.reject!(&:weak?)
99
+ end
100
+
101
+ def to_h
102
+ {
103
+ episode_count: @episodes.size,
104
+ thread_count: @threads.size,
105
+ self_concept: @self_concept.dup,
106
+ by_type: @episodes.group_by(&:episode_type).transform_values(&:size),
107
+ by_domain: @episodes.group_by(&:domain).transform_values(&:size),
108
+ avg_significance: avg_significance
109
+ }
110
+ end
111
+
112
+ private
113
+
114
+ def auto_link_threads(episode)
115
+ @threads.each do |thread|
116
+ relevance = episode.matches_tags?([thread.theme])
117
+ relevance += 0.2 if episode.domain == thread.domain
118
+ next unless relevance >= THREAD_MATCH_THRESHOLD
119
+
120
+ thread.add_episode(episode.id)
121
+ episode.link_thread(thread.id)
122
+ end
123
+ end
124
+
125
+ def update_self_concept(episode)
126
+ trait = episode.episode_type
127
+ current = @self_concept.fetch(trait, 0.0)
128
+ signal = episode.significance
129
+ @self_concept[trait] = ((TRAIT_ALPHA * signal) + ((1.0 - TRAIT_ALPHA) * current)).clamp(0.0, 1.0)
130
+ trim_self_concept
131
+ end
132
+
133
+ def trim_self_concept
134
+ return unless @self_concept.size > MAX_SELF_CONCEPT_TRAITS
135
+
136
+ sorted = @self_concept.sort_by { |_, v| v }
137
+ @self_concept = sorted.last(MAX_SELF_CONCEPT_TRAITS).to_h
138
+ end
139
+
140
+ def trim_episodes
141
+ return unless @episodes.size > MAX_EPISODES
142
+
143
+ @episodes.sort_by!(&:significance)
144
+ @episodes.shift(@episodes.size - MAX_EPISODES)
145
+ end
146
+
147
+ def trim_threads
148
+ return unless @threads.size > MAX_THREADS
149
+
150
+ @threads.sort_by!(&:strength)
151
+ @threads.shift(@threads.size - MAX_THREADS)
152
+ end
153
+
154
+ def episode_type_distribution
155
+ @episodes.group_by(&:episode_type)
156
+ .transform_values(&:size)
157
+ .sort_by { |_, count| -count }
158
+ end
159
+
160
+ def domain_distribution
161
+ @episodes.group_by(&:domain)
162
+ .transform_values(&:size)
163
+ .sort_by { |_, count| -count }
164
+ end
165
+
166
+ def avg_significance
167
+ return 0.0 if @episodes.empty?
168
+
169
+ @episodes.sum(&:significance) / @episodes.size
170
+ end
171
+
172
+ def narrative_richness
173
+ return 0.0 if @episodes.empty?
174
+
175
+ type_diversity = @episodes.map(&:episode_type).uniq.size.to_f / EPISODE_TYPES.size
176
+ thread_activity = @threads.empty? ? 0.0 : @threads.count { |t| t.size > 1 }.to_f / @threads.size
177
+ ((type_diversity + thread_activity) / 2.0).clamp(0.0, 1.0)
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module NarrativeSelf
6
+ module Helpers
7
+ module Constants
8
+ MAX_EPISODES = 500
9
+ MAX_THREADS = 50
10
+ MAX_CHAPTER_SIZE = 20
11
+ EPISODE_DECAY = 0.005
12
+ THREAD_DECAY = 0.01
13
+ SIGNIFICANCE_FLOOR = 0.05
14
+ SIGNIFICANCE_ALPHA = 0.15
15
+ DEFAULT_SIGNIFICANCE = 0.5
16
+ EMOTIONAL_BOOST = 0.3
17
+ THREAD_MATCH_THRESHOLD = 0.3
18
+ MAX_SELF_CONCEPT_TRAITS = 30
19
+ TRAIT_ALPHA = 0.1
20
+ MAX_TIMELINE_WINDOW = 100
21
+
22
+ SIGNIFICANCE_LABELS = {
23
+ (0.8..) => :pivotal,
24
+ (0.6...0.8) => :important,
25
+ (0.3...0.6) => :routine,
26
+ (..0.3) => :minor
27
+ }.freeze
28
+
29
+ EPISODE_TYPES = %i[
30
+ achievement failure discovery connection
31
+ conflict resolution insight surprise
32
+ decision transition reflection
33
+ ].freeze
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module NarrativeSelf
8
+ module Helpers
9
+ class Episode
10
+ include Constants
11
+
12
+ attr_reader :id, :description, :episode_type, :domain, :significance,
13
+ :emotional_valence, :tags, :created_at, :thread_ids
14
+
15
+ def initialize(description:, episode_type: :insight, domain: :general,
16
+ significance: nil, emotional_valence: 0.0, tags: [])
17
+ @id = SecureRandom.uuid
18
+ @description = description
19
+ @episode_type = episode_type
20
+ @domain = domain
21
+ @significance = significance || DEFAULT_SIGNIFICANCE
22
+ @emotional_valence = emotional_valence.clamp(-1.0, 1.0)
23
+ @tags = tags
24
+ @created_at = Time.now.utc
25
+ @thread_ids = []
26
+ end
27
+
28
+ def boost(amount)
29
+ emotional_factor = @emotional_valence.abs * EMOTIONAL_BOOST
30
+ @significance = [(@significance + amount + emotional_factor), 1.0].min
31
+ end
32
+
33
+ def decay
34
+ @significance = [(@significance - EPISODE_DECAY), 0.0].max
35
+ end
36
+
37
+ def faded?
38
+ @significance < SIGNIFICANCE_FLOOR
39
+ end
40
+
41
+ def label
42
+ SIGNIFICANCE_LABELS.each do |range, lbl|
43
+ return lbl if range.cover?(@significance)
44
+ end
45
+ :minor
46
+ end
47
+
48
+ def link_thread(thread_id)
49
+ @thread_ids << thread_id unless @thread_ids.include?(thread_id)
50
+ end
51
+
52
+ def matches_tags?(query_tags)
53
+ return false if query_tags.empty? || @tags.empty?
54
+
55
+ overlap = (@tags & query_tags).size
56
+ overlap.to_f / query_tags.size
57
+ end
58
+
59
+ def to_h
60
+ {
61
+ id: @id,
62
+ description: @description,
63
+ episode_type: @episode_type,
64
+ domain: @domain,
65
+ significance: @significance,
66
+ emotional_valence: @emotional_valence,
67
+ label: label,
68
+ tags: @tags.dup,
69
+ thread_ids: @thread_ids.dup,
70
+ created_at: @created_at
71
+ }
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module NarrativeSelf
8
+ module Helpers
9
+ class NarrativeThread
10
+ include Constants
11
+
12
+ attr_reader :id, :theme, :domain, :strength, :episode_ids, :created_at
13
+
14
+ def initialize(theme:, domain: :general)
15
+ @id = SecureRandom.uuid
16
+ @theme = theme
17
+ @domain = domain
18
+ @strength = 0.5
19
+ @episode_ids = []
20
+ @created_at = Time.now.utc
21
+ end
22
+
23
+ def add_episode(episode_id)
24
+ return if @episode_ids.include?(episode_id)
25
+
26
+ @episode_ids << episode_id
27
+ @episode_ids.shift if @episode_ids.size > MAX_CHAPTER_SIZE
28
+ reinforce
29
+ end
30
+
31
+ def reinforce
32
+ @strength = [@strength + 0.1, 1.0].min
33
+ end
34
+
35
+ def decay
36
+ @strength = [(@strength - THREAD_DECAY), 0.0].max
37
+ end
38
+
39
+ def weak?
40
+ @strength < SIGNIFICANCE_FLOOR
41
+ end
42
+
43
+ def size
44
+ @episode_ids.size
45
+ end
46
+
47
+ def to_h
48
+ {
49
+ id: @id,
50
+ theme: @theme,
51
+ domain: @domain,
52
+ strength: @strength,
53
+ episodes: @episode_ids.size,
54
+ created_at: @created_at
55
+ }
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module NarrativeSelf
6
+ module Runners
7
+ module NarrativeSelf
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def record_episode(description:, episode_type: :insight, domain: :general,
12
+ significance: nil, emotional_valence: 0.0, tags: [], **)
13
+ episode = autobiography.record_episode(
14
+ description: description,
15
+ episode_type: episode_type,
16
+ domain: domain,
17
+ significance: significance,
18
+ emotional_valence: emotional_valence,
19
+ tags: tags
20
+ )
21
+ Legion::Logging.debug "[narrative_self] recorded episode=#{episode_type} domain=#{domain} sig=#{episode.significance.round(3)}"
22
+ { success: true, episode: episode.to_h }
23
+ end
24
+
25
+ def recent_episodes(count: 10, **)
26
+ episodes = autobiography.recent_episodes(count)
27
+ { success: true, episodes: episodes.map(&:to_h), count: episodes.size }
28
+ end
29
+
30
+ def significant_episodes(min_significance: 0.6, **)
31
+ episodes = autobiography.significant_episodes(min_significance: min_significance)
32
+ { success: true, episodes: episodes.map(&:to_h), count: episodes.size }
33
+ end
34
+
35
+ def episodes_by_type(episode_type:, **)
36
+ episodes = autobiography.episodes_by_type(episode_type)
37
+ { success: true, episodes: episodes.map(&:to_h), count: episodes.size }
38
+ end
39
+
40
+ def create_thread(theme:, domain: :general, **)
41
+ thread = autobiography.create_thread(theme: theme, domain: domain)
42
+ Legion::Logging.debug "[narrative_self] created thread=#{theme} domain=#{domain}"
43
+ { success: true, thread: thread.to_h }
44
+ end
45
+
46
+ def strongest_threads(count: 5, **)
47
+ threads = autobiography.strongest_threads(count)
48
+ { success: true, threads: threads.map(&:to_h), count: threads.size }
49
+ end
50
+
51
+ def timeline(window: nil, **)
52
+ w = window || Helpers::Constants::MAX_TIMELINE_WINDOW
53
+ entries = autobiography.timeline(window: w)
54
+ { success: true, timeline: entries, count: entries.size }
55
+ end
56
+
57
+ def self_summary(**)
58
+ summary = autobiography.self_summary
59
+ Legion::Logging.debug "[narrative_self] summary: episodes=#{summary[:total_episodes]} richness=#{summary[:narrative_richness].round(3)}"
60
+ { success: true, summary: summary }
61
+ end
62
+
63
+ def update_narrative_self(**)
64
+ autobiography.decay_all
65
+ Legion::Logging.debug "[narrative_self] tick: episodes=#{autobiography.episodes.size} threads=#{autobiography.threads.size}"
66
+ { success: true, episode_count: autobiography.episodes.size, thread_count: autobiography.threads.size }
67
+ end
68
+
69
+ def narrative_self_stats(**)
70
+ { success: true, stats: autobiography.to_h }
71
+ end
72
+
73
+ private
74
+
75
+ def autobiography
76
+ @autobiography ||= Helpers::Autobiography.new
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module NarrativeSelf
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/narrative_self/version'
4
+ require 'legion/extensions/narrative_self/helpers/constants'
5
+ require 'legion/extensions/narrative_self/helpers/episode'
6
+ require 'legion/extensions/narrative_self/helpers/narrative_thread'
7
+ require 'legion/extensions/narrative_self/helpers/autobiography'
8
+ require 'legion/extensions/narrative_self/runners/narrative_self'
9
+ require 'legion/extensions/narrative_self/client'
10
+
11
+ module Legion
12
+ module Extensions
13
+ module NarrativeSelf
14
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
15
+ end
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-narrative-self
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matthew Iverson
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: Models autonoetic consciousness — maintains an autobiographical narrative
27
+ of episodes, narrative threads, and an evolving self-concept that emerges from the
28
+ pattern of lived experience.
29
+ email:
30
+ - matt@iverson.io
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - README.md
36
+ - lib/legion/extensions/narrative_self.rb
37
+ - lib/legion/extensions/narrative_self/client.rb
38
+ - lib/legion/extensions/narrative_self/helpers/autobiography.rb
39
+ - lib/legion/extensions/narrative_self/helpers/constants.rb
40
+ - lib/legion/extensions/narrative_self/helpers/episode.rb
41
+ - lib/legion/extensions/narrative_self/helpers/narrative_thread.rb
42
+ - lib/legion/extensions/narrative_self/runners/narrative_self.rb
43
+ - lib/legion/extensions/narrative_self/version.rb
44
+ homepage: https://github.com/LegionIO/lex-narrative-self
45
+ licenses:
46
+ - MIT
47
+ metadata:
48
+ rubygems_mfa_required: 'true'
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '3.4'
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubygems_version: 3.6.9
64
+ specification_version: 4
65
+ summary: Autobiographical narrative and self-concept for LegionIO
66
+ test_files: []