lex-agentic-self 0.1.10 → 0.1.11

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: '081b534c7eaf4efbc0265eada2d3b8f11b37b93a3fb34be77068d0ce4f0e842d'
4
- data.tar.gz: ce8a3839f617dc41d904ccf54a98a208037178cd89eefb39cd031fce8a89e315
3
+ metadata.gz: aa196a32fb12a9051c899a77c904db1b019bcf20fa722522f4560aa06325c1c5
4
+ data.tar.gz: 21ea04fbbd1a0bb7ccbaf1baeb93c062c036818cecd316f907d82ba4607fc640
5
5
  SHA512:
6
- metadata.gz: 49409e3e358267476770c66db88a4eff2f7968857c130e547cc3170a3c3e891b27358d7b28ac77e81e5733d480ab65480b38c10c908ef9323738b4159bab527e
7
- data.tar.gz: fd913ef78cfa8b14ccb5bdadbd95e3133e5bcf67d21a40332ba360ba6393ebd1b34e7db0ccebe3a4bf177aea506d1fc666dcb0efc89820e26853e0574888998d
6
+ metadata.gz: 2e5f6191d0093ad34c915a5368d71b1457f9774e393fff5f9d0424784ca38c250ce3c3ae3c3be6c0fc4c9b4a43ac8b2148bd3917b3ed37e102181aa2beae1e1a
7
+ data.tar.gz: 2bc178f3bfc1001ccc64af277a3e9fac685fea350964107883b6d001a4fe8da31fd3287b18e3a4b6838bd293798737c1c8eeb9fac3c77d44010be734e97279d0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.11] - 2026-03-31
4
+
5
+ ### Fixed
6
+ - `ArcEngine#to_h` now returns the last computed `relationship_health` score instead of always nil; `relationship_health()` caches result in `@last_health`
7
+ - `ArcEngine#arc_state_hash` now includes `milestones_today` key (milestones with `created_at` matching today in local time)
8
+ - `SelfTalk::Runners::SelfTalk#stub_turn_content` replaced by `mechanical_turn_content` backed by `VOICE_BANK` — produces real, meaningful content for critic/advocate/explorer/pragmatist voices; unknown types fall back to `VOICE_BANK_GENERIC`
9
+ - `SelfTalk::Runners::SelfTalk#generate_summary_for_dialogue` replaces static "Dialogue concluded" fallback with `mechanical_summary` — includes turn count, voices, and dominant position
10
+ - `Identity::Runners::Entra#rotate_client_secret` now emits a `Legion::Logging.warn` when `rotation_enabled: true` but Graph API rotation is not yet implemented, and returns `action_required` with instructions instead of a silent error
11
+
3
12
  ## [0.1.10] - 2026-03-31
4
13
 
5
14
  ### Added
data/Gemfile CHANGED
@@ -3,3 +3,5 @@
3
3
  source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
+
7
+ gem 'rubocop-legion'
@@ -34,7 +34,7 @@ Gem::Specification.new do |spec|
34
34
 
35
35
  spec.add_development_dependency 'faraday', '~> 2.0'
36
36
  spec.add_development_dependency 'rspec', '~> 3.13'
37
- spec.add_development_dependency 'rubocop', '~> 1.60'
38
- spec.add_development_dependency 'rubocop-legion', '~> 0.1'
39
- spec.add_development_dependency 'rubocop-rspec', '~> 2.26'
37
+ spec.add_development_dependency 'rubocop'
38
+ spec.add_development_dependency 'rubocop-legion'
39
+ spec.add_development_dependency 'rubocop-rspec'
40
40
  end
@@ -293,8 +293,11 @@ module Legion
293
293
 
294
294
  return { rotated: false, worker_id: worker_id, dry_run: true, would_rotate: true } if dry_run
295
295
 
296
- # Graph API rotation would go here when permission is granted
297
- { rotated: false, worker_id: worker_id, error: 'graph_api_rotation_not_implemented' }
296
+ # Graph API rotation is blocked on Azure Application.ReadWrite.All permission.
297
+ # Emit a loud warning so callers are never silently misled.
298
+ Legion::Logging.warn "[identity:entra] Client secret rotation is enabled but Graph API rotation is not yet implemented. Worker: #{worker_id}"
299
+ { rotated: false, worker_id: worker_id, error: 'graph_api_rotation_not_implemented',
300
+ action_required: 'Manual rotation needed — Graph API write permission not yet granted' }
298
301
  end
299
302
 
300
303
  def credential_refresh_cycle(**)
@@ -41,7 +41,7 @@ module Legion
41
41
  score = (attachment_strength.to_f * w[:attachment_strength]) +
42
42
  (reciprocity_balance.to_f * w[:reciprocity_balance]) +
43
43
  (communication_consistency.to_f * w[:communication_consistency])
44
- score.clamp(0.0, 1.0)
44
+ @last_health = score.clamp(0.0, 1.0)
45
45
  end
46
46
 
47
47
  def dirty?
@@ -77,14 +77,27 @@ module Legion
77
77
  def to_h
78
78
  { agent_id: @agent_id, current_chapter: @current_chapter,
79
79
  milestones: @milestones.map(&:to_h),
80
- relationship_health: nil, milestone_count: @milestones.size }
80
+ relationship_health: @last_health, milestone_count: @milestones.size }
81
81
  end
82
82
 
83
83
  private
84
84
 
85
85
  def arc_state_hash
86
86
  { agent_id: @agent_id, current_chapter: @current_chapter,
87
- milestones: @milestones.map(&:to_h) }
87
+ milestones: @milestones.map(&:to_h),
88
+ milestones_today: @milestones.select { |m| milestone_today?(m) }.map(&:to_h) }
89
+ end
90
+
91
+ def milestone_today?(milestone)
92
+ ts = milestone.respond_to?(:created_at) ? milestone.created_at : milestone[:created_at]
93
+ return false unless ts
94
+
95
+ today = ::Time.now
96
+ t = ts.is_a?(::Time) ? ts.localtime : ::Time.parse(ts.to_s)
97
+ t.year == today.year && t.mon == today.mon && t.mday == today.mday
98
+ rescue StandardError => e
99
+ warn "[arc_engine] milestone_today? error: #{e.message}"
100
+ false
88
101
  end
89
102
 
90
103
  def derive_chapter(bond_stage)
@@ -104,6 +104,35 @@ module Legion
104
104
  { decayed: decayed, voices: voice_list }
105
105
  end
106
106
 
107
+ VOICE_BANK = {
108
+ critic: [
109
+ { content: 'What could go wrong with this approach?', position: :challenge },
110
+ { content: 'Are we overlooking any risks here?', position: :challenge },
111
+ { content: 'This needs more careful consideration.', position: :caution }
112
+ ],
113
+ advocate: [
114
+ { content: 'This aligns with our core values.', position: :support },
115
+ { content: 'The potential benefits outweigh the risks.', position: :support },
116
+ { content: 'We should move forward with this.', position: :affirm }
117
+ ],
118
+ explorer: [
119
+ { content: 'What alternatives have we not considered?', position: :explore },
120
+ { content: 'There may be an unconventional approach here.', position: :explore },
121
+ { content: 'Let us examine this from another angle.', position: :clarify }
122
+ ],
123
+ pragmatist: [
124
+ { content: 'What is the simplest path forward?', position: :simplify },
125
+ { content: 'Focus on what is actionable now.', position: :prioritize },
126
+ { content: 'We need concrete next steps.', position: :clarify }
127
+ ]
128
+ }.freeze
129
+
130
+ VOICE_BANK_GENERIC = [
131
+ { content: 'Let us think this through carefully.', position: :clarify },
132
+ { content: 'More reflection is needed on this topic.', position: :clarify },
133
+ { content: 'What do we know for certain here?', position: :clarify }
134
+ ].freeze
135
+
107
136
  private
108
137
 
109
138
  def engine
@@ -124,7 +153,7 @@ module Legion
124
153
  )
125
154
  return [llm_result, :llm] if llm_result
126
155
  end
127
- [stub_turn_content(voice_data.voice_type, dialogue_data.topic), :mechanical]
156
+ [mechanical_turn_content(voice_data.voice_type, dialogue_data.topic), :mechanical]
128
157
  end
129
158
 
130
159
  def build_prior_turns(dialogue_data)
@@ -135,13 +164,14 @@ module Legion
135
164
  end
136
165
  end
137
166
 
138
- def stub_turn_content(voice_type, topic)
139
- { content: "[#{voice_type} perspective on #{topic}]", position: :clarify }
167
+ def mechanical_turn_content(voice_type, _topic)
168
+ bank = VOICE_BANK.fetch(voice_type.to_sym, VOICE_BANK_GENERIC)
169
+ bank.sample
140
170
  end
141
171
 
142
172
  def generate_summary_for_dialogue(dialogue_id)
143
173
  dialogue_data = engine.dialogues[dialogue_id]
144
- return 'Dialogue concluded' unless dialogue_data
174
+ return mechanical_summary(nil) unless dialogue_data
145
175
 
146
176
  if Helpers::LlmEnhancer.available?
147
177
  turns = dialogue_data.turns.map do |t|
@@ -161,7 +191,15 @@ module Legion
161
191
  return llm_result[:summary] if llm_result
162
192
  end
163
193
 
164
- 'Dialogue concluded'
194
+ mechanical_summary(dialogue_data)
195
+ end
196
+
197
+ def mechanical_summary(dialogue_data)
198
+ turns = dialogue_data&.turns || []
199
+ voices = turns.filter_map { |t| t.respond_to?(:voice_id) ? engine.voices[t.voice_id]&.name || t.voice_id : t[:voice_id] }.uniq
200
+ positions = turns.filter_map { |t| t.respond_to?(:position) ? t.position : t[:position] }.tally
201
+ dominant = positions.max_by { |_, count| count }&.first
202
+ "#{turns.size} turns across #{voices.join(', ')} voices. Dominant position: #{dominant || 'none'}."
165
203
  end
166
204
  end
167
205
  end
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Agentic
6
6
  module Self
7
- VERSION = '0.1.10'
7
+ VERSION = '0.1.11'
8
8
  end
9
9
  end
10
10
  end
@@ -338,6 +338,59 @@ RSpec.describe Legion::Extensions::Agentic::Self::Identity::Runners::Entra do
338
338
  expect(result[:action_required]).to be false
339
339
  expect(result[:days_remaining]).to be > 30
340
340
  end
341
+
342
+ context 'when rotation_enabled is true and secret is expiring (fix 5)' do
343
+ before do
344
+ stub_const('Legion::Settings', Class.new do
345
+ def self.dig(*keys)
346
+ map = {
347
+ %i[identity entra rotation_enabled] => true,
348
+ %i[identity entra rotation_buffer_days] => 30
349
+ }
350
+ map[keys]
351
+ end
352
+
353
+ def self.[](_key) = {}
354
+ end)
355
+ end
356
+
357
+ it 'emits a warning log when graph api rotation is not implemented' do
358
+ vault_mod = Module.new do
359
+ def self.read_client_secret(**)
360
+ { client_secret: 'val', client_secret_expires_at: (Time.now + (86_400 * 10)).iso8601 }
361
+ end
362
+ end
363
+ stub_const('Legion::Extensions::Agentic::Self::Identity::Helpers::VaultSecrets', vault_mod)
364
+
365
+ warned = false
366
+ allow(Legion::Logging).to receive(:warn) do |msg|
367
+ warned = true if msg.include?('graph_api_rotation_not_implemented') ||
368
+ msg.include?('Graph API rotation is not yet implemented')
369
+ end
370
+
371
+ result = client.rotate_client_secret(worker_id: 'w1')
372
+
373
+ expect(warned).to be true
374
+ expect(result[:rotated]).to be false
375
+ expect(result[:error]).to eq('graph_api_rotation_not_implemented')
376
+ expect(result[:action_required]).to include('Manual rotation needed')
377
+ end
378
+
379
+ it 'returns dry_run result without warning when dry_run: true' do
380
+ vault_mod = Module.new do
381
+ def self.read_client_secret(**)
382
+ { client_secret: 'val', client_secret_expires_at: (Time.now + (86_400 * 10)).iso8601 }
383
+ end
384
+ end
385
+ stub_const('Legion::Extensions::Agentic::Self::Identity::Helpers::VaultSecrets', vault_mod)
386
+
387
+ expect(Legion::Logging).not_to receive(:warn).with(a_string_including('not yet implemented'))
388
+
389
+ result = client.rotate_client_secret(worker_id: 'w1', dry_run: true)
390
+ expect(result[:dry_run]).to be true
391
+ expect(result[:would_rotate]).to be true
392
+ end
393
+ end
341
394
  end
342
395
 
343
396
  # ---------------------------------------------------------------------------
@@ -120,5 +120,46 @@ RSpec.describe Legion::Extensions::Agentic::Self::RelationshipArc::Helpers::ArcE
120
120
  h = engine.to_h
121
121
  expect(h).to include(:agent_id, :current_chapter, :milestones, :relationship_health)
122
122
  end
123
+
124
+ it 'relationship_health is nil before relationship_health() is called' do
125
+ expect(engine.to_h[:relationship_health]).to be_nil
126
+ end
127
+
128
+ it 'relationship_health reflects the last computed value' do
129
+ engine.relationship_health(
130
+ attachment_strength: 0.8,
131
+ reciprocity_balance: 0.6,
132
+ communication_consistency: 0.7
133
+ )
134
+ h = engine.to_h
135
+ expected = (0.8 * 0.4) + (0.6 * 0.3) + (0.7 * 0.3)
136
+ expect(h[:relationship_health]).to be_within(0.01).of(expected)
137
+ end
138
+
139
+ it 'includes milestone_count' do
140
+ engine.add_milestone(type: :first_interaction, description: 'x', significance: 0.5)
141
+ expect(engine.to_h[:milestone_count]).to eq(1)
142
+ end
143
+ end
144
+
145
+ describe '#arc_state_hash (via to_apollo_entries)' do
146
+ it 'includes milestones_today for milestones added today' do
147
+ engine.add_milestone(type: :first_interaction, description: 'today', significance: 0.9)
148
+ entries = engine.to_apollo_entries
149
+ parsed = JSON.parse(entries.first[:content], symbolize_names: true)
150
+ expect(parsed).to have_key(:milestones_today)
151
+ expect(parsed[:milestones_today].size).to eq(1)
152
+ end
153
+
154
+ it 'milestones_today is empty when milestones have old timestamps' do
155
+ ms = Legion::Extensions::Agentic::Self::RelationshipArc::Helpers::Milestone.new(
156
+ type: :first_interaction, description: 'old', significance: 0.5,
157
+ created_at: Time.now.utc - (2 * 86_400)
158
+ )
159
+ engine.instance_variable_get(:@milestones) << ms
160
+ entries = engine.to_apollo_entries
161
+ parsed = JSON.parse(entries.first[:content], symbolize_names: true)
162
+ expect(parsed[:milestones_today]).to eq([])
163
+ end
123
164
  end
124
165
  end
@@ -137,7 +137,7 @@ RSpec.describe Legion::Extensions::Agentic::Self::SelfTalk::Runners::SelfTalk, '
137
137
  result = client.conclude_dialogue(dialogue_id: did)
138
138
  expect(result[:concluded]).to be true
139
139
  report = client.dialogue_report(dialogue_id: did)
140
- expect(report[:dialogue][:conclusion]).to eq('Dialogue concluded')
140
+ expect(report[:dialogue][:conclusion]).to match(/turns across .* voices\. Dominant position:/)
141
141
  end
142
142
  end
143
143
 
@@ -151,7 +151,7 @@ RSpec.describe Legion::Extensions::Agentic::Self::SelfTalk::Runners::SelfTalk, '
151
151
  result = client.conclude_dialogue(dialogue_id: did)
152
152
  expect(result[:concluded]).to be true
153
153
  report = client.dialogue_report(dialogue_id: did)
154
- expect(report[:dialogue][:conclusion]).to eq('Dialogue concluded')
154
+ expect(report[:dialogue][:conclusion]).to match(/turns across .* voices\. Dominant position:/)
155
155
  end
156
156
  end
157
157
 
@@ -193,4 +193,64 @@ RSpec.describe Legion::Extensions::Agentic::Self::SelfTalk::Runners::SelfTalk do
193
193
  expect(voice[:name]).to eq('Critic')
194
194
  end
195
195
  end
196
+
197
+ describe 'mechanical voice bank (fix 3)' do
198
+ let(:runner_class) do
199
+ Class.new do
200
+ include Legion::Extensions::Agentic::Self::SelfTalk::Runners::SelfTalk
201
+ end
202
+ end
203
+ let(:runner) { runner_class.new }
204
+
205
+ it 'produces real content for known voice types' do
206
+ %i[critic advocate explorer pragmatist].each do |type|
207
+ result = runner.send(:mechanical_turn_content, type, 'test topic')
208
+ expect(result[:content]).not_to be_empty
209
+ expect(result[:content]).not_to match(/\A\[/)
210
+ expect(result[:position]).not_to be_nil
211
+ end
212
+ end
213
+
214
+ it 'falls back to generic bank for unknown voice types' do
215
+ result = runner.send(:mechanical_turn_content, :unknown_voice, 'some topic')
216
+ expect(result[:content]).not_to be_empty
217
+ expect(result[:position]).not_to be_nil
218
+ end
219
+
220
+ it 'VOICE_BANK covers critic, advocate, explorer, pragmatist' do
221
+ bank = Legion::Extensions::Agentic::Self::SelfTalk::Runners::SelfTalk::VOICE_BANK
222
+ expect(bank.keys).to include(:critic, :advocate, :explorer, :pragmatist)
223
+ end
224
+ end
225
+
226
+ describe 'mechanical summary (fix 4)' do
227
+ let(:runner_class) do
228
+ Class.new do
229
+ include Legion::Extensions::Agentic::Self::SelfTalk::Runners::SelfTalk
230
+ end
231
+ end
232
+ let(:runner) { runner_class.new }
233
+
234
+ it 'produces a non-empty summary when dialogue_data is nil' do
235
+ summary = runner.send(:mechanical_summary, nil)
236
+ expect(summary).to be_a(String)
237
+ expect(summary).not_to be_empty
238
+ end
239
+
240
+ it 'includes turn count in summary' do
241
+ did = start_dialogue(topic: 'summary test')
242
+ vid = register_voice(name: 'Critic', type: :critic)
243
+ client.add_turn(dialogue_id: did, voice_id: vid, content: 'Point one', position: :challenge)
244
+ client.add_turn(dialogue_id: did, voice_id: vid, content: 'Point two', position: :challenge)
245
+
246
+ result = client.conclude_dialogue(dialogue_id: did)
247
+ expect(result[:concluded]).to be true
248
+ end
249
+
250
+ it 'conclude_dialogue without summary uses mechanical_summary' do
251
+ did = start_dialogue(topic: 'no summary')
252
+ result = client.conclude_dialogue(dialogue_id: did)
253
+ expect(result[:concluded]).to be true
254
+ end
255
+ end
196
256
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-agentic-self
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.10
4
+ version: 0.1.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -139,44 +139,44 @@ dependencies:
139
139
  name: rubocop
140
140
  requirement: !ruby/object:Gem::Requirement
141
141
  requirements:
142
- - - "~>"
142
+ - - ">="
143
143
  - !ruby/object:Gem::Version
144
- version: '1.60'
144
+ version: '0'
145
145
  type: :development
146
146
  prerelease: false
147
147
  version_requirements: !ruby/object:Gem::Requirement
148
148
  requirements:
149
- - - "~>"
149
+ - - ">="
150
150
  - !ruby/object:Gem::Version
151
- version: '1.60'
151
+ version: '0'
152
152
  - !ruby/object:Gem::Dependency
153
153
  name: rubocop-legion
154
154
  requirement: !ruby/object:Gem::Requirement
155
155
  requirements:
156
- - - "~>"
156
+ - - ">="
157
157
  - !ruby/object:Gem::Version
158
- version: '0.1'
158
+ version: '0'
159
159
  type: :development
160
160
  prerelease: false
161
161
  version_requirements: !ruby/object:Gem::Requirement
162
162
  requirements:
163
- - - "~>"
163
+ - - ">="
164
164
  - !ruby/object:Gem::Version
165
- version: '0.1'
165
+ version: '0'
166
166
  - !ruby/object:Gem::Dependency
167
167
  name: rubocop-rspec
168
168
  requirement: !ruby/object:Gem::Requirement
169
169
  requirements:
170
- - - "~>"
170
+ - - ">="
171
171
  - !ruby/object:Gem::Version
172
- version: '2.26'
172
+ version: '0'
173
173
  type: :development
174
174
  prerelease: false
175
175
  version_requirements: !ruby/object:Gem::Requirement
176
176
  requirements:
177
- - - "~>"
177
+ - - ">="
178
178
  - !ruby/object:Gem::Version
179
- version: '2.26'
179
+ version: '0'
180
180
  description: 'LEX agentic self domain: identity, metacognition, self-model'
181
181
  email:
182
182
  - matthewdiverson@gmail.com