lex-narrative-identity 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/.github/workflows/ci.yml +16 -0
- data/.gitignore +2 -0
- data/.rspec +3 -0
- data/.rubocop.yml +55 -0
- data/CLAUDE.md +201 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +78 -0
- data/README.md +100 -0
- data/lex-narrative-identity.gemspec +31 -0
- data/lib/legion/extensions/narrative_identity/actors/narrative_decay.rb +41 -0
- data/lib/legion/extensions/narrative_identity/client.rb +18 -0
- data/lib/legion/extensions/narrative_identity/helpers/chapter.rb +44 -0
- data/lib/legion/extensions/narrative_identity/helpers/constants.rb +58 -0
- data/lib/legion/extensions/narrative_identity/helpers/episode.rb +63 -0
- data/lib/legion/extensions/narrative_identity/helpers/narrative_engine.rb +183 -0
- data/lib/legion/extensions/narrative_identity/helpers/theme.rb +46 -0
- data/lib/legion/extensions/narrative_identity/runners/narrative_identity.rb +154 -0
- data/lib/legion/extensions/narrative_identity/version.rb +9 -0
- data/lib/legion/extensions/narrative_identity.rb +18 -0
- metadata +81 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 0a602ec01c1c966f890208189c8f3c365dbddb1bd0100918969897173321edb0
|
|
4
|
+
data.tar.gz: 871f36359814855aa0b4638fa309b78140c972afc2266b230df3c65a6c3f3646
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1a4730fb8952762856ee765890bd66859dc816676d131f6d851537f204a010c9d68e1a7ca15eb22ccb29c66c3c6e38396dfff77fe2bead2de4cc975afd2791b7
|
|
7
|
+
data.tar.gz: eb50d9072c08f998553ce26fe45eea2d38ba7032baef93de3cd49c44349a4fc80a8c93fc85eff2988dc1380b16078dff9a22b70c952636060d0fd92d7bd3c090
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
ci:
|
|
9
|
+
uses: LegionIO/.github/.github/workflows/ci.yml@main
|
|
10
|
+
|
|
11
|
+
release:
|
|
12
|
+
needs: ci
|
|
13
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
14
|
+
uses: LegionIO/.github/.github/workflows/release.yml@main
|
|
15
|
+
secrets:
|
|
16
|
+
rubygems-api-key: ${{ secrets.RUBYGEMS_API_KEY }}
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
TargetRubyVersion: 3.4
|
|
3
|
+
NewCops: enable
|
|
4
|
+
SuggestExtensions: false
|
|
5
|
+
|
|
6
|
+
Layout/LineLength:
|
|
7
|
+
Max: 160
|
|
8
|
+
|
|
9
|
+
Layout/SpaceAroundEqualsInParameterDefault:
|
|
10
|
+
EnforcedStyle: space
|
|
11
|
+
|
|
12
|
+
Layout/HashAlignment:
|
|
13
|
+
EnforcedHashRocketStyle: table
|
|
14
|
+
EnforcedColonStyle: table
|
|
15
|
+
|
|
16
|
+
Metrics/MethodLength:
|
|
17
|
+
Max: 25
|
|
18
|
+
|
|
19
|
+
Metrics/ClassLength:
|
|
20
|
+
Max: 150
|
|
21
|
+
|
|
22
|
+
Metrics/ModuleLength:
|
|
23
|
+
Max: 150
|
|
24
|
+
|
|
25
|
+
Metrics/BlockLength:
|
|
26
|
+
Max: 40
|
|
27
|
+
Exclude:
|
|
28
|
+
- 'spec/**/*'
|
|
29
|
+
|
|
30
|
+
Metrics/AbcSize:
|
|
31
|
+
Max: 25
|
|
32
|
+
|
|
33
|
+
Metrics/ParameterLists:
|
|
34
|
+
Max: 8
|
|
35
|
+
MaxOptionalParameters: 8
|
|
36
|
+
|
|
37
|
+
Style/Documentation:
|
|
38
|
+
Enabled: false
|
|
39
|
+
|
|
40
|
+
Style/FrozenStringLiteralComment:
|
|
41
|
+
Enabled: true
|
|
42
|
+
EnforcedStyle: always
|
|
43
|
+
|
|
44
|
+
Naming/FileName:
|
|
45
|
+
Enabled: false
|
|
46
|
+
|
|
47
|
+
Naming/PredicateMethod:
|
|
48
|
+
Enabled: false
|
|
49
|
+
|
|
50
|
+
Naming/PredicatePrefix:
|
|
51
|
+
Enabled: false
|
|
52
|
+
|
|
53
|
+
Style/OneClassPerFile:
|
|
54
|
+
Exclude:
|
|
55
|
+
- 'spec/spec_helper.rb'
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# lex-narrative-identity
|
|
2
|
+
|
|
3
|
+
**Level 3 Leaf Documentation**
|
|
4
|
+
- **Parent**: `/Users/miverso2/rubymine/legion/extensions-agentic/CLAUDE.md`
|
|
5
|
+
- **Gem**: `lex-narrative-identity`
|
|
6
|
+
- **Version**: `0.1.0`
|
|
7
|
+
- **Namespace**: `Legion::Extensions::NarrativeIdentity`
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
Personal narrative construction and identity coherence for LegionIO agents. Records significant episodes (achievements, failures, discoveries, transformations, etc.) organized into chapters and themed arcs. Computes narrative coherence as a measure of identity stability. Builds a life story from the accumulated episode record. Theme weights evolve with each episode, decaying toward zero and reinforced when engaged. Provides the agent's autobiographical "who am I" answer.
|
|
12
|
+
|
|
13
|
+
## Gem Info
|
|
14
|
+
|
|
15
|
+
- **Require path**: `legion/extensions/narrative_identity`
|
|
16
|
+
- **Ruby**: >= 3.4
|
|
17
|
+
- **License**: MIT
|
|
18
|
+
- **Registers with**: `Legion::Extensions::Core`
|
|
19
|
+
|
|
20
|
+
## File Structure
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
lib/legion/extensions/narrative_identity/
|
|
24
|
+
version.rb
|
|
25
|
+
helpers/
|
|
26
|
+
constants.rb # Episode types, theme types, significance labels, coherence labels
|
|
27
|
+
episode.rb # Episode value object
|
|
28
|
+
theme.rb # Theme value object with weight
|
|
29
|
+
chapter.rb # Chapter value object with open/close lifecycle
|
|
30
|
+
narrative_engine.rb # NarrativeEngine with story construction + coherence
|
|
31
|
+
actors/
|
|
32
|
+
narrative_decay.rb # Theme decay actor
|
|
33
|
+
runners/
|
|
34
|
+
narrative_identity.rb # Runner module (uses extend self pattern)
|
|
35
|
+
|
|
36
|
+
spec/
|
|
37
|
+
legion/extensions/narrative_identity/
|
|
38
|
+
helpers/
|
|
39
|
+
constants_spec.rb
|
|
40
|
+
episode_spec.rb
|
|
41
|
+
theme_spec.rb
|
|
42
|
+
chapter_spec.rb
|
|
43
|
+
narrative_engine_spec.rb
|
|
44
|
+
actors/narrative_decay_spec.rb
|
|
45
|
+
runners/narrative_identity_spec.rb
|
|
46
|
+
spec_helper.rb
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Key Constants
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
MAX_EPISODES = 500
|
|
53
|
+
MAX_THEMES = 50
|
|
54
|
+
MAX_CHAPTERS = 20
|
|
55
|
+
|
|
56
|
+
EPISODE_TYPES = %i[
|
|
57
|
+
achievement failure discovery relationship challenge transformation routine
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
THEME_TYPES = %i[
|
|
61
|
+
growth agency communion redemption contamination stability exploration
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
SIGNIFICANCE_LABELS = {
|
|
65
|
+
(0.8..) => :pivotal,
|
|
66
|
+
(0.6...0.8) => :significant,
|
|
67
|
+
(0.4...0.6) => :notable,
|
|
68
|
+
(0.2...0.4) => :minor,
|
|
69
|
+
(..0.2) => :incidental
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
COHERENCE_LABELS = {
|
|
73
|
+
(0.8..) => :coherent,
|
|
74
|
+
(0.6...0.8) => :mostly_coherent,
|
|
75
|
+
(0.4...0.6) => :fragmented,
|
|
76
|
+
(0.2...0.4) => :incoherent,
|
|
77
|
+
(..0.2) => :disintegrated
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
CHAPTER_LABELS = %i[origin early_learning growth mastery current]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Helpers
|
|
84
|
+
|
|
85
|
+
### `Helpers::Episode` (class)
|
|
86
|
+
|
|
87
|
+
A significant event in the agent's history.
|
|
88
|
+
|
|
89
|
+
| Attribute | Type | Description |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `id` | String (UUID) | unique identifier |
|
|
92
|
+
| `type` | Symbol | from EPISODE_TYPES |
|
|
93
|
+
| `content` | String | description of the episode |
|
|
94
|
+
| `significance` | Float (0..1) | how pivotal this episode was |
|
|
95
|
+
| `emotional_valence` | Float (-1..1) | emotional charge |
|
|
96
|
+
| `themes` | Array<Symbol> | associated theme IDs |
|
|
97
|
+
| `chapter_id` | String | chapter this episode belongs to |
|
|
98
|
+
| `occurred_at` | Time | when it happened |
|
|
99
|
+
|
|
100
|
+
### `Helpers::Theme` (class)
|
|
101
|
+
|
|
102
|
+
A recurring narrative arc threading through episodes.
|
|
103
|
+
|
|
104
|
+
| Attribute | Type | Description |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| `id` | String (UUID) | unique identifier |
|
|
107
|
+
| `name` | Symbol | from THEME_TYPES or custom symbol |
|
|
108
|
+
| `weight` | Float (0..1) | current prominence in the narrative |
|
|
109
|
+
| `episode_count` | Integer | episodes linked to this theme |
|
|
110
|
+
|
|
111
|
+
Key methods:
|
|
112
|
+
- `reinforce(amount)` — weight += amount (cap 1.0); increments episode_count
|
|
113
|
+
- `decay(amount)` — weight -= amount (floor 0.0); removes theme from narrative when weight reaches 0
|
|
114
|
+
|
|
115
|
+
### `Helpers::Chapter` (class)
|
|
116
|
+
|
|
117
|
+
A named temporal segment of the agent's story.
|
|
118
|
+
|
|
119
|
+
| Attribute | Type | Description |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| `id` | String (UUID) | unique identifier |
|
|
122
|
+
| `label` | Symbol | from CHAPTER_LABELS |
|
|
123
|
+
| `title` | String | chapter title |
|
|
124
|
+
| `opened_at` | Time | start of chapter |
|
|
125
|
+
| `closed_at` | Time | end of chapter (nil if current) |
|
|
126
|
+
| `episode_ids` | Array<String> | episodes belonging to this chapter |
|
|
127
|
+
|
|
128
|
+
Key methods:
|
|
129
|
+
- `open` — sets opened_at, marks as active
|
|
130
|
+
- `close` — sets closed_at, marks as completed
|
|
131
|
+
- `current?` — closed_at.nil?
|
|
132
|
+
|
|
133
|
+
### `Helpers::NarrativeEngine` (class)
|
|
134
|
+
|
|
135
|
+
Central narrative store and story construction.
|
|
136
|
+
|
|
137
|
+
| Method | Description |
|
|
138
|
+
|---|---|
|
|
139
|
+
| `add_episode(type:, content:, significance:, emotional_valence:)` | creates and stores episode; enforces MAX_EPISODES |
|
|
140
|
+
| `assign_to_chapter(episode_id:, chapter_id:)` | links episode to chapter |
|
|
141
|
+
| `create_chapter(label:, title:)` | creates chapter; enforces MAX_CHAPTERS |
|
|
142
|
+
| `close_chapter(chapter_id:)` | closes chapter |
|
|
143
|
+
| `add_theme(name:)` | creates theme; enforces MAX_THEMES |
|
|
144
|
+
| `link_theme(episode_id:, theme_id:)` | associates episode with theme |
|
|
145
|
+
| `reinforce_theme(theme_id:, amount:)` | boosts theme weight |
|
|
146
|
+
| `narrative_coherence` | coherence score: significance-weighted variance across emotional valences |
|
|
147
|
+
| `identity_summary` | snapshot: current chapter, dominant themes, coherence, most significant episode |
|
|
148
|
+
| `life_story` | ordered episode content array with chapter grouping |
|
|
149
|
+
| `most_defining_episodes(limit:)` | top N episodes by significance |
|
|
150
|
+
| `prominent_themes(limit:)` | top N themes by weight |
|
|
151
|
+
| `current_chapter` | the chapter with closed_at == nil |
|
|
152
|
+
| `decay_all_themes!` | decrements all theme weights by small amount |
|
|
153
|
+
| `narrative_report` | full report: episode counts, themes, coherence, chapters |
|
|
154
|
+
|
|
155
|
+
Coherence formula: `1.0 - (significance-weighted stddev of emotional_valence)`. High coherence = consistent emotional arc; low coherence = wildly varying emotional experiences.
|
|
156
|
+
|
|
157
|
+
## Actors
|
|
158
|
+
|
|
159
|
+
**`Actors::NarrativeDecay`** — fires periodically, calls `decay_themes` on the runner to decay all theme weights. Note: actor name is `NarrativeDecay`, not `Decay`.
|
|
160
|
+
|
|
161
|
+
## Runners
|
|
162
|
+
|
|
163
|
+
Module: `Legion::Extensions::NarrativeIdentity::Runners::NarrativeIdentity`
|
|
164
|
+
|
|
165
|
+
Note: The runner uses `extend self` rather than the standard module-method pattern used by other LEX runners. This means its methods are called directly on the module.
|
|
166
|
+
|
|
167
|
+
Private state: `@engine` (memoized `NarrativeEngine` instance via `@engine ||= NarrativeEngine.new`).
|
|
168
|
+
|
|
169
|
+
| Runner Method | Parameters | Description |
|
|
170
|
+
|---|---|---|
|
|
171
|
+
| `record_episode` | `type:, content:, significance: 0.5, emotional_valence: 0.0` | Record a significant episode |
|
|
172
|
+
| `assign_episode_to_chapter` | `episode_id:, chapter_id:` | Link episode to chapter |
|
|
173
|
+
| `create_chapter` | `label:, title:` | Create a new narrative chapter |
|
|
174
|
+
| `close_chapter` | `chapter_id:` | Close a chapter |
|
|
175
|
+
| `add_theme` | `name:` | Add a narrative theme |
|
|
176
|
+
| `link_theme` | `episode_id:, theme_id:` | Link episode to theme |
|
|
177
|
+
| `reinforce_theme` | `theme_id:, amount: 0.1` | Boost theme prominence |
|
|
178
|
+
| `narrative_coherence` | (none) | Narrative coherence score and label |
|
|
179
|
+
| `identity_summary` | (none) | Who-am-I snapshot |
|
|
180
|
+
| `life_story` | (none) | Full ordered episode content |
|
|
181
|
+
| `most_defining_episodes` | `limit: 5` | Top N by significance |
|
|
182
|
+
| `prominent_themes` | `limit: 5` | Top N by weight |
|
|
183
|
+
| `current_chapter` | (none) | The currently active chapter |
|
|
184
|
+
| `decay_themes` | (none) | Decay all theme weights |
|
|
185
|
+
| `narrative_report` | (none) | Full narrative stats |
|
|
186
|
+
|
|
187
|
+
## Integration Points
|
|
188
|
+
|
|
189
|
+
- **lex-memory**: significant episodic memory traces from lex-memory are the raw material for episode creation; callers extract high-significance/high-emotional-intensity traces and `record_episode` them.
|
|
190
|
+
- **lex-mental-time-travel**: temporal journeys that revisit significant episodes reinforce the narrative by increasing those episodes' coherence contribution.
|
|
191
|
+
- **lex-dream**: dream phase `agenda_formation` uses narrative themes to set priorities; prominent themes guide dream consolidation.
|
|
192
|
+
- **lex-metacognition**: `NarrativeIdentity` is listed under `:introspection` capability category.
|
|
193
|
+
|
|
194
|
+
## Development Notes
|
|
195
|
+
|
|
196
|
+
- Runner uses `extend self` — this is the only runner in all 25 LEX gems that uses this pattern instead of the standard module with instance methods called via `Client`. The effect is that runner methods are module-level methods, not mixed into an object.
|
|
197
|
+
- Actor name is `NarrativeDecay`, not the standard `Decay` used by other LEX gems with decay actors. When referencing, use `Actors::NarrativeDecay`.
|
|
198
|
+
- Coherence is a stability measure, not a richness measure. An agent with entirely negative experiences has high coherence (consistent arc); an agent with alternating positive/negative experiences has low coherence.
|
|
199
|
+
- `narrative_report` is the most comprehensive output — includes episode counts by type, theme weights, coherence score, chapter list, and most defining episodes.
|
|
200
|
+
- MAX_EPISODES eviction removes oldest incidental (lowest significance) episodes first, not oldest by time. This preserves pivotal moments regardless of age.
|
|
201
|
+
- Theme decay is slow but episodic — themes with no recent `reinforce_theme` calls will gradually fade. The relationship between decay rate and number of episodes needed to sustain a theme depends on the actor interval.
|
data/Gemfile
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
source 'https://rubygems.org'
|
|
4
|
+
|
|
5
|
+
gemspec
|
|
6
|
+
|
|
7
|
+
group :test do
|
|
8
|
+
gem 'rspec', '~> 3.13'
|
|
9
|
+
gem 'rubocop', '~> 1.75', require: false
|
|
10
|
+
gem 'rubocop-rspec', require: false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
gem 'legion-gaia', path: '../../legion-gaia'
|
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
lex-narrative-identity (0.1.0)
|
|
5
|
+
|
|
6
|
+
GEM
|
|
7
|
+
remote: https://rubygems.org/
|
|
8
|
+
specs:
|
|
9
|
+
addressable (2.8.9)
|
|
10
|
+
public_suffix (>= 2.0.2, < 8.0)
|
|
11
|
+
ast (2.4.3)
|
|
12
|
+
bigdecimal (4.0.1)
|
|
13
|
+
diff-lcs (1.6.2)
|
|
14
|
+
json (2.19.1)
|
|
15
|
+
json-schema (6.2.0)
|
|
16
|
+
addressable (~> 2.8)
|
|
17
|
+
bigdecimal (>= 3.1, < 5)
|
|
18
|
+
language_server-protocol (3.17.0.5)
|
|
19
|
+
lint_roller (1.1.0)
|
|
20
|
+
mcp (0.8.0)
|
|
21
|
+
json-schema (>= 4.1)
|
|
22
|
+
parallel (1.27.0)
|
|
23
|
+
parser (3.3.10.2)
|
|
24
|
+
ast (~> 2.4.1)
|
|
25
|
+
racc
|
|
26
|
+
prism (1.9.0)
|
|
27
|
+
public_suffix (7.0.5)
|
|
28
|
+
racc (1.8.1)
|
|
29
|
+
rainbow (3.1.1)
|
|
30
|
+
regexp_parser (2.11.3)
|
|
31
|
+
rspec (3.13.2)
|
|
32
|
+
rspec-core (~> 3.13.0)
|
|
33
|
+
rspec-expectations (~> 3.13.0)
|
|
34
|
+
rspec-mocks (~> 3.13.0)
|
|
35
|
+
rspec-core (3.13.6)
|
|
36
|
+
rspec-support (~> 3.13.0)
|
|
37
|
+
rspec-expectations (3.13.5)
|
|
38
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
39
|
+
rspec-support (~> 3.13.0)
|
|
40
|
+
rspec-mocks (3.13.8)
|
|
41
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
42
|
+
rspec-support (~> 3.13.0)
|
|
43
|
+
rspec-support (3.13.7)
|
|
44
|
+
rubocop (1.85.1)
|
|
45
|
+
json (~> 2.3)
|
|
46
|
+
language_server-protocol (~> 3.17.0.2)
|
|
47
|
+
lint_roller (~> 1.1.0)
|
|
48
|
+
mcp (~> 0.6)
|
|
49
|
+
parallel (~> 1.10)
|
|
50
|
+
parser (>= 3.3.0.2)
|
|
51
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
52
|
+
regexp_parser (>= 2.9.3, < 3.0)
|
|
53
|
+
rubocop-ast (>= 1.49.0, < 2.0)
|
|
54
|
+
ruby-progressbar (~> 1.7)
|
|
55
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
|
56
|
+
rubocop-ast (1.49.1)
|
|
57
|
+
parser (>= 3.3.7.2)
|
|
58
|
+
prism (~> 1.7)
|
|
59
|
+
rubocop-rspec (3.9.0)
|
|
60
|
+
lint_roller (~> 1.1)
|
|
61
|
+
rubocop (~> 1.81)
|
|
62
|
+
ruby-progressbar (1.13.0)
|
|
63
|
+
unicode-display_width (3.2.0)
|
|
64
|
+
unicode-emoji (~> 4.1)
|
|
65
|
+
unicode-emoji (4.2.0)
|
|
66
|
+
|
|
67
|
+
PLATFORMS
|
|
68
|
+
arm64-darwin-25
|
|
69
|
+
ruby
|
|
70
|
+
|
|
71
|
+
DEPENDENCIES
|
|
72
|
+
lex-narrative-identity!
|
|
73
|
+
rspec (~> 3.13)
|
|
74
|
+
rubocop (~> 1.75)
|
|
75
|
+
rubocop-rspec
|
|
76
|
+
|
|
77
|
+
BUNDLED WITH
|
|
78
|
+
2.6.9
|
data/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# lex-narrative-identity
|
|
2
|
+
|
|
3
|
+
Personal narrative construction and identity coherence for LegionIO agents. Part of the LegionIO cognitive architecture extension ecosystem (LEX).
|
|
4
|
+
|
|
5
|
+
## What It Does
|
|
6
|
+
|
|
7
|
+
`lex-narrative-identity` gives an agent an autobiographical self. It records significant episodes (achievements, failures, discoveries, transformations) organized into chapters and threaded by recurring themes. Narrative coherence measures identity stability based on the consistency of the agent's emotional arc. The life story and identity summary answer the agent's "who am I" question.
|
|
8
|
+
|
|
9
|
+
Key capabilities:
|
|
10
|
+
|
|
11
|
+
- **Episode types**: achievement, failure, discovery, relationship, challenge, transformation, routine
|
|
12
|
+
- **Theme types**: growth, agency, communion, redemption, contamination, stability, exploration
|
|
13
|
+
- **Chapters**: temporal narrative segments with label, title, and open/closed lifecycle
|
|
14
|
+
- **Coherence scoring**: significance-weighted variance of emotional valence across episodes
|
|
15
|
+
- **Coherence labels**: coherent / mostly_coherent / fragmented / incoherent / disintegrated
|
|
16
|
+
- **Significance labels**: pivotal / significant / notable / minor / incidental
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
Add to your Gemfile:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
gem 'lex-narrative-identity'
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or install directly:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
gem install lex-narrative-identity
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
require 'legion/extensions/narrative_identity'
|
|
36
|
+
|
|
37
|
+
client = Legion::Extensions::NarrativeIdentity::Client.new
|
|
38
|
+
|
|
39
|
+
# Create a narrative chapter
|
|
40
|
+
chapter = client.create_chapter(label: :growth, title: 'Learning Phase')
|
|
41
|
+
chapter_id = chapter[:chapter][:id]
|
|
42
|
+
|
|
43
|
+
# Record significant episodes
|
|
44
|
+
ep = client.record_episode(
|
|
45
|
+
type: :achievement,
|
|
46
|
+
content: 'Successfully resolved a complex multi-agent coordination problem',
|
|
47
|
+
significance: 0.9,
|
|
48
|
+
emotional_valence: 0.8
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Assign to chapter and link themes
|
|
52
|
+
client.assign_episode_to_chapter(episode_id: ep[:episode][:id], chapter_id: chapter_id)
|
|
53
|
+
|
|
54
|
+
theme = client.add_theme(name: :growth)
|
|
55
|
+
client.link_theme(episode_id: ep[:episode][:id], theme_id: theme[:theme][:id])
|
|
56
|
+
|
|
57
|
+
# Get identity summary
|
|
58
|
+
summary = client.identity_summary
|
|
59
|
+
# => { coherence: 0.82, coherence_label: :coherent,
|
|
60
|
+
# dominant_themes: [:growth], current_chapter: { label: :growth, ... },
|
|
61
|
+
# most_significant: { content: '...', significance: 0.9 } }
|
|
62
|
+
|
|
63
|
+
# Full life story
|
|
64
|
+
client.life_story
|
|
65
|
+
|
|
66
|
+
# Stats
|
|
67
|
+
client.narrative_report
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Runner Methods
|
|
71
|
+
|
|
72
|
+
| Method | Description |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `record_episode` | Record a significant episode |
|
|
75
|
+
| `assign_episode_to_chapter` | Link episode to a chapter |
|
|
76
|
+
| `create_chapter` | Create a new narrative chapter |
|
|
77
|
+
| `close_chapter` | Close a chapter |
|
|
78
|
+
| `add_theme` | Add a narrative theme |
|
|
79
|
+
| `link_theme` | Link an episode to a theme |
|
|
80
|
+
| `reinforce_theme` | Boost a theme's prominence |
|
|
81
|
+
| `narrative_coherence` | Coherence score and label |
|
|
82
|
+
| `identity_summary` | Who-am-I snapshot: coherence, themes, current chapter |
|
|
83
|
+
| `life_story` | Full ordered episode content with chapter grouping |
|
|
84
|
+
| `most_defining_episodes` | Top N episodes by significance |
|
|
85
|
+
| `prominent_themes` | Top N themes by weight |
|
|
86
|
+
| `current_chapter` | The currently active chapter |
|
|
87
|
+
| `decay_themes` | Decay all theme weights |
|
|
88
|
+
| `narrative_report` | Full narrative statistics |
|
|
89
|
+
|
|
90
|
+
## Development
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
bundle install
|
|
94
|
+
bundle exec rspec
|
|
95
|
+
bundle exec rubocop
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/narrative_identity/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'lex-narrative-identity'
|
|
7
|
+
spec.version = Legion::Extensions::NarrativeIdentity::VERSION
|
|
8
|
+
spec.authors = ['Esity']
|
|
9
|
+
spec.email = ['matthewdiverson@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'LEX Narrative Identity'
|
|
12
|
+
spec.description = 'Autobiographical narrative identity for LegionIO — the agent constructs and ' \
|
|
13
|
+
'maintains a life narrative of who it is, what it has done, and what it values, ' \
|
|
14
|
+
'based on Dan McAdams narrative identity theory'
|
|
15
|
+
spec.homepage = 'https://github.com/LegionIO/lex-narrative-identity'
|
|
16
|
+
spec.license = 'MIT'
|
|
17
|
+
spec.required_ruby_version = '>= 3.4'
|
|
18
|
+
|
|
19
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
20
|
+
spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-narrative-identity'
|
|
21
|
+
spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-narrative-identity'
|
|
22
|
+
spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-narrative-identity'
|
|
23
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-narrative-identity/issues'
|
|
24
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
25
|
+
|
|
26
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
27
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
28
|
+
end
|
|
29
|
+
spec.require_paths = ['lib']
|
|
30
|
+
spec.add_development_dependency 'legion-gaia'
|
|
31
|
+
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 NarrativeIdentity
|
|
8
|
+
module Actor
|
|
9
|
+
class NarrativeDecay < Legion::Extensions::Actors::Every
|
|
10
|
+
def runner_class
|
|
11
|
+
Legion::Extensions::NarrativeIdentity::Runners::NarrativeIdentity
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def runner_function
|
|
15
|
+
'decay_themes'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def time
|
|
19
|
+
600
|
|
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,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module NarrativeIdentity
|
|
6
|
+
class Client
|
|
7
|
+
include Runners::NarrativeIdentity
|
|
8
|
+
|
|
9
|
+
attr_reader :engine
|
|
10
|
+
|
|
11
|
+
def initialize(engine: nil)
|
|
12
|
+
@engine = engine || Helpers::NarrativeEngine.new
|
|
13
|
+
@default_engine = @engine
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module NarrativeIdentity
|
|
8
|
+
module Helpers
|
|
9
|
+
class Chapter
|
|
10
|
+
attr_reader :id, :title, :label, :episode_ids, :start_time
|
|
11
|
+
attr_accessor :end_time
|
|
12
|
+
|
|
13
|
+
def initialize(title:, label:, episode_ids: nil, start_time: nil, end_time: nil, id: nil)
|
|
14
|
+
@id = id || SecureRandom.uuid
|
|
15
|
+
@title = title
|
|
16
|
+
@label = label
|
|
17
|
+
@episode_ids = episode_ids || []
|
|
18
|
+
@start_time = start_time || Time.now.utc
|
|
19
|
+
@end_time = end_time
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def current?
|
|
23
|
+
@end_time.nil?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def episode_count
|
|
27
|
+
@episode_ids.size
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_h
|
|
31
|
+
{
|
|
32
|
+
id: @id,
|
|
33
|
+
title: @title,
|
|
34
|
+
label: @label,
|
|
35
|
+
episode_ids: @episode_ids.dup,
|
|
36
|
+
start_time: @start_time,
|
|
37
|
+
end_time: @end_time
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module NarrativeIdentity
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
MAX_EPISODES = 500
|
|
9
|
+
MAX_THEMES = 50
|
|
10
|
+
MAX_CHAPTERS = 20
|
|
11
|
+
|
|
12
|
+
EPISODE_TYPES = %i[
|
|
13
|
+
achievement
|
|
14
|
+
failure
|
|
15
|
+
discovery
|
|
16
|
+
relationship
|
|
17
|
+
challenge
|
|
18
|
+
transformation
|
|
19
|
+
routine
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
22
|
+
THEME_TYPES = %i[
|
|
23
|
+
growth
|
|
24
|
+
agency
|
|
25
|
+
communion
|
|
26
|
+
redemption
|
|
27
|
+
contamination
|
|
28
|
+
stability
|
|
29
|
+
exploration
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
EMOTIONAL_VALENCE_WEIGHT = 0.4
|
|
33
|
+
SIGNIFICANCE_WEIGHT = 0.3
|
|
34
|
+
RECENCY_WEIGHT = 0.3
|
|
35
|
+
COHERENCE_DECAY = 0.01
|
|
36
|
+
|
|
37
|
+
SIGNIFICANCE_LABELS = [
|
|
38
|
+
{ (0.8..1.0) => :defining },
|
|
39
|
+
{ (0.6...0.8) => :major },
|
|
40
|
+
{ (0.4...0.6) => :notable },
|
|
41
|
+
{ (0.2...0.4) => :minor },
|
|
42
|
+
{ (0.0...0.2) => :trivial }
|
|
43
|
+
].freeze
|
|
44
|
+
|
|
45
|
+
COHERENCE_LABELS = [
|
|
46
|
+
{ (0.8..1.0) => :unified },
|
|
47
|
+
{ (0.6...0.8) => :coherent },
|
|
48
|
+
{ (0.4...0.6) => :developing },
|
|
49
|
+
{ (0.2...0.4) => :fragmented },
|
|
50
|
+
{ (0.0...0.2) => :absent }
|
|
51
|
+
].freeze
|
|
52
|
+
|
|
53
|
+
CHAPTER_LABELS = %i[origin early_learning growth mastery current].freeze
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module NarrativeIdentity
|
|
8
|
+
module Helpers
|
|
9
|
+
class Episode
|
|
10
|
+
attr_reader :id, :content, :episode_type, :emotional_valence,
|
|
11
|
+
:significance, :domain, :themes, :created_at
|
|
12
|
+
attr_accessor :chapter_id
|
|
13
|
+
|
|
14
|
+
def initialize(content:, episode_type:, emotional_valence:, significance:, domain:, **)
|
|
15
|
+
@id = SecureRandom.uuid
|
|
16
|
+
@content = content
|
|
17
|
+
@episode_type = episode_type
|
|
18
|
+
@emotional_valence = emotional_valence.clamp(-1.0, 1.0)
|
|
19
|
+
@significance = significance.clamp(0.0, 1.0)
|
|
20
|
+
@domain = domain
|
|
21
|
+
@chapter_id = nil
|
|
22
|
+
@themes = []
|
|
23
|
+
@created_at = Time.now.utc
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def positive?
|
|
27
|
+
@emotional_valence.positive?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def negative?
|
|
31
|
+
@emotional_valence.negative?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def defining?
|
|
35
|
+
@significance >= 0.8
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def significance_label
|
|
39
|
+
Constants::SIGNIFICANCE_LABELS.each do |entry|
|
|
40
|
+
range, label = entry.first
|
|
41
|
+
return label if range.cover?(@significance)
|
|
42
|
+
end
|
|
43
|
+
:trivial
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def to_h
|
|
47
|
+
{
|
|
48
|
+
id: @id,
|
|
49
|
+
content: @content,
|
|
50
|
+
episode_type: @episode_type,
|
|
51
|
+
emotional_valence: @emotional_valence,
|
|
52
|
+
significance: @significance,
|
|
53
|
+
domain: @domain,
|
|
54
|
+
chapter_id: @chapter_id,
|
|
55
|
+
themes: @themes.dup,
|
|
56
|
+
created_at: @created_at
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module NarrativeIdentity
|
|
6
|
+
module Helpers
|
|
7
|
+
class NarrativeEngine
|
|
8
|
+
attr_reader :episodes, :themes, :chapters
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@episodes = {}
|
|
12
|
+
@themes = {}
|
|
13
|
+
@chapters = {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def add_episode(content:, episode_type:, emotional_valence:, significance:, domain:)
|
|
17
|
+
prune_episodes! if @episodes.size >= Constants::MAX_EPISODES
|
|
18
|
+
episode = Episode.new(
|
|
19
|
+
content: content,
|
|
20
|
+
episode_type: episode_type,
|
|
21
|
+
emotional_valence: emotional_valence,
|
|
22
|
+
significance: significance,
|
|
23
|
+
domain: domain
|
|
24
|
+
)
|
|
25
|
+
@episodes[episode.id] = episode
|
|
26
|
+
episode
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def assign_to_chapter(episode_id:, chapter_id:)
|
|
30
|
+
episode = @episodes.fetch(episode_id, nil)
|
|
31
|
+
chapter = @chapters.fetch(chapter_id, nil)
|
|
32
|
+
return false unless episode && chapter
|
|
33
|
+
|
|
34
|
+
episode.chapter_id = chapter_id
|
|
35
|
+
chapter.episode_ids << episode_id unless chapter.episode_ids.include?(episode_id)
|
|
36
|
+
true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def create_chapter(title:, label:)
|
|
40
|
+
prune_chapters! if @chapters.size >= Constants::MAX_CHAPTERS
|
|
41
|
+
chapter = Chapter.new(title: title, label: label)
|
|
42
|
+
@chapters[chapter.id] = chapter
|
|
43
|
+
chapter
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def close_chapter(chapter_id:)
|
|
47
|
+
chapter = @chapters.fetch(chapter_id, nil)
|
|
48
|
+
return false unless chapter
|
|
49
|
+
|
|
50
|
+
chapter.end_time = Time.now.utc
|
|
51
|
+
true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def add_theme(name:, theme_type:)
|
|
55
|
+
prune_themes! if @themes.size >= Constants::MAX_THEMES
|
|
56
|
+
theme = Theme.new(name: name, theme_type: theme_type)
|
|
57
|
+
@themes[theme.id] = theme
|
|
58
|
+
theme
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def link_theme(episode_id:, theme_id:)
|
|
62
|
+
episode = @episodes.fetch(episode_id, nil)
|
|
63
|
+
theme = @themes.fetch(theme_id, nil)
|
|
64
|
+
return false unless episode && theme
|
|
65
|
+
|
|
66
|
+
episode.themes << theme_id unless episode.themes.include?(theme_id)
|
|
67
|
+
theme.episode_ids << episode_id unless theme.episode_ids.include?(episode_id)
|
|
68
|
+
true
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def reinforce_theme(theme_id:, amount:)
|
|
72
|
+
theme = @themes.fetch(theme_id, nil)
|
|
73
|
+
return false unless theme
|
|
74
|
+
|
|
75
|
+
theme.reinforce!(amount)
|
|
76
|
+
true
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def narrative_coherence
|
|
80
|
+
return 0.0 if @episodes.empty? || @themes.empty?
|
|
81
|
+
|
|
82
|
+
linked = @episodes.values.count { |ep| ep.themes.any? }
|
|
83
|
+
base = (linked.to_f / @episodes.size).round(10)
|
|
84
|
+
theme_factor = prominent_themes.size.to_f / [@themes.size, 1].max
|
|
85
|
+
((base * 0.7) + (theme_factor * 0.3)).round(10).clamp(0.0, 1.0)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def identity_summary
|
|
89
|
+
{
|
|
90
|
+
top_themes: prominent_themes.first(5).map(&:to_h),
|
|
91
|
+
defining_episodes: most_defining_episodes(limit: 5).map(&:to_h),
|
|
92
|
+
current_chapter: current_chapter&.to_h,
|
|
93
|
+
coherence: narrative_coherence,
|
|
94
|
+
coherence_label: coherence_label,
|
|
95
|
+
episode_count: @episodes.size,
|
|
96
|
+
theme_count: @themes.size,
|
|
97
|
+
chapter_count: @chapters.size
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def life_story
|
|
102
|
+
@chapters.values.sort_by(&:start_time).map do |chapter|
|
|
103
|
+
eps = chapter.episode_ids.filter_map { |id| @episodes[id] }.sort_by(&:created_at)
|
|
104
|
+
{
|
|
105
|
+
chapter: chapter.to_h,
|
|
106
|
+
episodes: eps.map { |ep| ep.to_h.merge(theme_names: theme_names_for(ep)) }
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def most_defining_episodes(limit: 5)
|
|
112
|
+
@episodes.values.sort_by { |ep| -ep.significance }.first(limit)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def prominent_themes
|
|
116
|
+
@themes.values.select(&:prominent?).sort_by { |t| -t.strength }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def current_chapter
|
|
120
|
+
@chapters.values.find(&:current?)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def decay_all_themes!
|
|
124
|
+
@themes.each_value(&:decay!)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def narrative_report
|
|
128
|
+
{
|
|
129
|
+
identity_summary: identity_summary,
|
|
130
|
+
life_story: life_story,
|
|
131
|
+
narrative_state: {
|
|
132
|
+
coherence: narrative_coherence,
|
|
133
|
+
coherence_label: coherence_label,
|
|
134
|
+
prominent_themes: prominent_themes.map(&:to_h),
|
|
135
|
+
defining_episodes: most_defining_episodes(limit: 3).map(&:to_h),
|
|
136
|
+
current_chapter: current_chapter&.to_h
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def to_h
|
|
142
|
+
{
|
|
143
|
+
episodes: @episodes.transform_values(&:to_h),
|
|
144
|
+
themes: @themes.transform_values(&:to_h),
|
|
145
|
+
chapters: @chapters.transform_values(&:to_h)
|
|
146
|
+
}
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
def coherence_label
|
|
152
|
+
score = narrative_coherence
|
|
153
|
+
Constants::COHERENCE_LABELS.each do |entry|
|
|
154
|
+
range, label = entry.first
|
|
155
|
+
return label if range.cover?(score)
|
|
156
|
+
end
|
|
157
|
+
:absent
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def theme_names_for(episode)
|
|
161
|
+
episode.themes.filter_map { |tid| @themes[tid]&.name }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def prune_episodes!
|
|
165
|
+
oldest = @episodes.values.min_by(&:created_at)
|
|
166
|
+
@episodes.delete(oldest.id) if oldest
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def prune_themes!
|
|
170
|
+
weakest = @themes.values.min_by(&:strength)
|
|
171
|
+
@themes.delete(weakest.id) if weakest
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def prune_chapters!
|
|
175
|
+
closed = @chapters.values.reject(&:current?).min_by(&:start_time)
|
|
176
|
+
target = closed || @chapters.values.min_by(&:start_time)
|
|
177
|
+
@chapters.delete(target.id) if target
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module NarrativeIdentity
|
|
8
|
+
module Helpers
|
|
9
|
+
class Theme
|
|
10
|
+
attr_reader :id, :name, :theme_type, :episode_ids
|
|
11
|
+
attr_accessor :strength
|
|
12
|
+
|
|
13
|
+
def initialize(name:, theme_type:, strength: 0.0, episode_ids: nil, id: nil)
|
|
14
|
+
@id = id || SecureRandom.uuid
|
|
15
|
+
@name = name
|
|
16
|
+
@theme_type = theme_type
|
|
17
|
+
@strength = strength.clamp(0.0, 1.0)
|
|
18
|
+
@episode_ids = episode_ids || []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def reinforce!(amount)
|
|
22
|
+
@strength = (@strength + amount).clamp(0.0, 1.0).round(10)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def decay!
|
|
26
|
+
@strength = (@strength - Constants::COHERENCE_DECAY).clamp(0.0, 1.0).round(10)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def prominent?
|
|
30
|
+
@strength >= 0.6
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_h
|
|
34
|
+
{
|
|
35
|
+
id: @id,
|
|
36
|
+
name: @name,
|
|
37
|
+
theme_type: @theme_type,
|
|
38
|
+
strength: @strength,
|
|
39
|
+
episode_ids: @episode_ids.dup
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module NarrativeIdentity
|
|
6
|
+
module Runners
|
|
7
|
+
module NarrativeIdentity
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
def record_episode(content:, episode_type:, emotional_valence:, significance:, domain:,
|
|
11
|
+
engine: nil, **)
|
|
12
|
+
return { success: false, error: "unknown episode_type: #{episode_type.inspect}" } unless
|
|
13
|
+
Helpers::Constants::EPISODE_TYPES.include?(episode_type)
|
|
14
|
+
|
|
15
|
+
episode = resolve_engine(engine).add_episode(
|
|
16
|
+
content: content, episode_type: episode_type,
|
|
17
|
+
emotional_valence: emotional_valence, significance: significance, domain: domain
|
|
18
|
+
)
|
|
19
|
+
Legion::Logging.debug "[narrative_identity] recorded episode #{episode.id[0..7]} type=#{episode_type}"
|
|
20
|
+
{ success: true, episode: episode.to_h }
|
|
21
|
+
rescue StandardError => e
|
|
22
|
+
{ success: false, error: e.message }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def assign_episode_to_chapter(episode_id:, chapter_id:, engine: nil, **)
|
|
26
|
+
result = resolve_engine(engine).assign_to_chapter(episode_id: episode_id, chapter_id: chapter_id)
|
|
27
|
+
Legion::Logging.debug "[narrative_identity] assign episode=#{episode_id[0..7]} ok=#{result}"
|
|
28
|
+
{ success: result, episode_id: episode_id, chapter_id: chapter_id }
|
|
29
|
+
rescue StandardError => e
|
|
30
|
+
{ success: false, error: e.message }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def create_chapter(title:, label:, engine: nil, **)
|
|
34
|
+
return { success: false, error: "unknown chapter label: #{label.inspect}" } unless
|
|
35
|
+
Helpers::Constants::CHAPTER_LABELS.include?(label)
|
|
36
|
+
|
|
37
|
+
chapter = resolve_engine(engine).create_chapter(title: title, label: label)
|
|
38
|
+
Legion::Logging.debug "[narrative_identity] created chapter #{chapter.id[0..7]} label=#{label}"
|
|
39
|
+
{ success: true, chapter: chapter.to_h }
|
|
40
|
+
rescue StandardError => e
|
|
41
|
+
{ success: false, error: e.message }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def close_chapter(chapter_id:, engine: nil, **)
|
|
45
|
+
result = resolve_engine(engine).close_chapter(chapter_id: chapter_id)
|
|
46
|
+
Legion::Logging.debug "[narrative_identity] close_chapter #{chapter_id[0..7]} ok=#{result}"
|
|
47
|
+
{ success: result, chapter_id: chapter_id }
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
{ success: false, error: e.message }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def add_theme(name:, theme_type:, engine: nil, **)
|
|
53
|
+
return { success: false, error: "unknown theme_type: #{theme_type.inspect}" } unless
|
|
54
|
+
Helpers::Constants::THEME_TYPES.include?(theme_type)
|
|
55
|
+
|
|
56
|
+
theme = resolve_engine(engine).add_theme(name: name, theme_type: theme_type)
|
|
57
|
+
Legion::Logging.debug "[narrative_identity] added theme #{theme.id[0..7]} type=#{theme_type}"
|
|
58
|
+
{ success: true, theme: theme.to_h }
|
|
59
|
+
rescue StandardError => e
|
|
60
|
+
{ success: false, error: e.message }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def link_theme(episode_id:, theme_id:, engine: nil, **)
|
|
64
|
+
result = resolve_engine(engine).link_theme(episode_id: episode_id, theme_id: theme_id)
|
|
65
|
+
Legion::Logging.debug "[narrative_identity] link theme=#{theme_id[0..7]} episode=#{episode_id[0..7]} ok=#{result}"
|
|
66
|
+
{ success: result, episode_id: episode_id, theme_id: theme_id }
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
{ success: false, error: e.message }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def reinforce_theme(theme_id:, amount:, engine: nil, **)
|
|
72
|
+
result = resolve_engine(engine).reinforce_theme(theme_id: theme_id, amount: amount)
|
|
73
|
+
Legion::Logging.debug "[narrative_identity] reinforce theme=#{theme_id[0..7]} amount=#{amount} ok=#{result}"
|
|
74
|
+
{ success: result, theme_id: theme_id, amount: amount }
|
|
75
|
+
rescue StandardError => e
|
|
76
|
+
{ success: false, error: e.message }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def narrative_coherence(engine: nil, **)
|
|
80
|
+
score = resolve_engine(engine).narrative_coherence
|
|
81
|
+
Legion::Logging.debug "[narrative_identity] coherence=#{score}"
|
|
82
|
+
{ success: true, coherence: score }
|
|
83
|
+
rescue StandardError => e
|
|
84
|
+
{ success: false, error: e.message }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def identity_summary(engine: nil, **)
|
|
88
|
+
summary = resolve_engine(engine).identity_summary
|
|
89
|
+
Legion::Logging.debug '[narrative_identity] identity_summary requested'
|
|
90
|
+
{ success: true, summary: summary }
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
{ success: false, error: e.message }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def life_story(engine: nil, **)
|
|
96
|
+
story = resolve_engine(engine).life_story
|
|
97
|
+
Legion::Logging.debug "[narrative_identity] life_story chapters=#{story.size}"
|
|
98
|
+
{ success: true, life_story: story }
|
|
99
|
+
rescue StandardError => e
|
|
100
|
+
{ success: false, error: e.message }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def most_defining_episodes(limit: 5, engine: nil, **)
|
|
104
|
+
episodes = resolve_engine(engine).most_defining_episodes(limit: limit)
|
|
105
|
+
Legion::Logging.debug "[narrative_identity] defining_episodes count=#{episodes.size}"
|
|
106
|
+
{ success: true, episodes: episodes.map(&:to_h) }
|
|
107
|
+
rescue StandardError => e
|
|
108
|
+
{ success: false, error: e.message }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def prominent_themes(engine: nil, **)
|
|
112
|
+
themes = resolve_engine(engine).prominent_themes
|
|
113
|
+
Legion::Logging.debug "[narrative_identity] prominent_themes count=#{themes.size}"
|
|
114
|
+
{ success: true, themes: themes.map(&:to_h) }
|
|
115
|
+
rescue StandardError => e
|
|
116
|
+
{ success: false, error: e.message }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def current_chapter(engine: nil, **)
|
|
120
|
+
chapter = resolve_engine(engine).current_chapter
|
|
121
|
+
Legion::Logging.debug "[narrative_identity] current_chapter=#{chapter&.id&.slice(0..7)}"
|
|
122
|
+
{ success: true, chapter: chapter&.to_h }
|
|
123
|
+
rescue StandardError => e
|
|
124
|
+
{ success: false, error: e.message }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def decay_themes(engine: nil, **)
|
|
128
|
+
resolve_engine(engine).decay_all_themes!
|
|
129
|
+
Legion::Logging.debug '[narrative_identity] theme decay applied'
|
|
130
|
+
{ success: true }
|
|
131
|
+
rescue StandardError => e
|
|
132
|
+
{ success: false, error: e.message }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def narrative_report(engine: nil, **)
|
|
136
|
+
report = resolve_engine(engine).narrative_report
|
|
137
|
+
Legion::Logging.debug '[narrative_identity] narrative_report generated'
|
|
138
|
+
{ success: true, report: report }
|
|
139
|
+
rescue StandardError => e
|
|
140
|
+
{ success: false, error: e.message }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
include Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers::Lex)
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
def resolve_engine(engine)
|
|
148
|
+
engine || (@default_engine ||= Helpers::NarrativeEngine.new)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/narrative_identity/version'
|
|
4
|
+
require 'legion/extensions/narrative_identity/helpers/constants'
|
|
5
|
+
require 'legion/extensions/narrative_identity/helpers/episode'
|
|
6
|
+
require 'legion/extensions/narrative_identity/helpers/theme'
|
|
7
|
+
require 'legion/extensions/narrative_identity/helpers/chapter'
|
|
8
|
+
require 'legion/extensions/narrative_identity/helpers/narrative_engine'
|
|
9
|
+
require 'legion/extensions/narrative_identity/runners/narrative_identity'
|
|
10
|
+
require 'legion/extensions/narrative_identity/client'
|
|
11
|
+
|
|
12
|
+
module Legion
|
|
13
|
+
module Extensions
|
|
14
|
+
module NarrativeIdentity
|
|
15
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-narrative-identity
|
|
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: Autobiographical narrative identity for LegionIO — the agent constructs
|
|
27
|
+
and maintains a life narrative of who it is, what it has done, and what it values,
|
|
28
|
+
based on Dan McAdams narrative identity theory
|
|
29
|
+
email:
|
|
30
|
+
- matthewdiverson@gmail.com
|
|
31
|
+
executables: []
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- ".github/workflows/ci.yml"
|
|
36
|
+
- ".gitignore"
|
|
37
|
+
- ".rspec"
|
|
38
|
+
- ".rubocop.yml"
|
|
39
|
+
- CLAUDE.md
|
|
40
|
+
- Gemfile
|
|
41
|
+
- Gemfile.lock
|
|
42
|
+
- README.md
|
|
43
|
+
- lex-narrative-identity.gemspec
|
|
44
|
+
- lib/legion/extensions/narrative_identity.rb
|
|
45
|
+
- lib/legion/extensions/narrative_identity/actors/narrative_decay.rb
|
|
46
|
+
- lib/legion/extensions/narrative_identity/client.rb
|
|
47
|
+
- lib/legion/extensions/narrative_identity/helpers/chapter.rb
|
|
48
|
+
- lib/legion/extensions/narrative_identity/helpers/constants.rb
|
|
49
|
+
- lib/legion/extensions/narrative_identity/helpers/episode.rb
|
|
50
|
+
- lib/legion/extensions/narrative_identity/helpers/narrative_engine.rb
|
|
51
|
+
- lib/legion/extensions/narrative_identity/helpers/theme.rb
|
|
52
|
+
- lib/legion/extensions/narrative_identity/runners/narrative_identity.rb
|
|
53
|
+
- lib/legion/extensions/narrative_identity/version.rb
|
|
54
|
+
homepage: https://github.com/LegionIO/lex-narrative-identity
|
|
55
|
+
licenses:
|
|
56
|
+
- MIT
|
|
57
|
+
metadata:
|
|
58
|
+
homepage_uri: https://github.com/LegionIO/lex-narrative-identity
|
|
59
|
+
source_code_uri: https://github.com/LegionIO/lex-narrative-identity
|
|
60
|
+
documentation_uri: https://github.com/LegionIO/lex-narrative-identity
|
|
61
|
+
changelog_uri: https://github.com/LegionIO/lex-narrative-identity
|
|
62
|
+
bug_tracker_uri: https://github.com/LegionIO/lex-narrative-identity/issues
|
|
63
|
+
rubygems_mfa_required: 'true'
|
|
64
|
+
rdoc_options: []
|
|
65
|
+
require_paths:
|
|
66
|
+
- lib
|
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
68
|
+
requirements:
|
|
69
|
+
- - ">="
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '3.4'
|
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
73
|
+
requirements:
|
|
74
|
+
- - ">="
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: '0'
|
|
77
|
+
requirements: []
|
|
78
|
+
rubygems_version: 3.6.9
|
|
79
|
+
specification_version: 4
|
|
80
|
+
summary: LEX Narrative Identity
|
|
81
|
+
test_files: []
|