lex-agentic-memory 0.1.19 → 0.1.21
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 +4 -4
- data/CHANGELOG.md +14 -0
- data/Gemfile +2 -0
- data/lex-agentic-memory.gemspec +3 -3
- data/lib/legion/extensions/agentic/memory/communication_pattern/client.rb +15 -0
- data/lib/legion/extensions/agentic/memory/communication_pattern/helpers/constants.rb +26 -0
- data/lib/legion/extensions/agentic/memory/communication_pattern/helpers/pattern_analyzer.rb +157 -0
- data/lib/legion/extensions/agentic/memory/communication_pattern/runners/communication_pattern.rb +55 -0
- data/lib/legion/extensions/agentic/memory/communication_pattern/version.rb +13 -0
- data/lib/legion/extensions/agentic/memory/communication_pattern.rb +7 -0
- data/lib/legion/extensions/agentic/memory/trace/helpers/error_tracer.rb +26 -16
- data/lib/legion/extensions/agentic/memory/version.rb +1 -1
- data/lib/legion/extensions/agentic/memory.rb +1 -0
- data/spec/legion/extensions/agentic/memory/communication_pattern/helpers/constants_spec.rb +36 -0
- data/spec/legion/extensions/agentic/memory/communication_pattern/helpers/pattern_analyzer_spec.rb +132 -0
- data/spec/legion/extensions/agentic/memory/communication_pattern/runners/communication_pattern_spec.rb +66 -0
- data/spec/legion/extensions/agentic/memory/trace/helpers/error_tracer_spec.rb +128 -0
- metadata +22 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f403d34cba75eaf3f800a8852c8d6e16d2f56924b2454b7c444a0cee82a00f04
|
|
4
|
+
data.tar.gz: f946558e6cf3103bc32bf44d9accb9a4c3a13d91342e7a06a5f6b5ec98c0c856
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e49ff76e259d4237f19a7998f8e973438ebcfceb44322990a6213f35bd07481adfea5f8cd037c351217af569931f2344ceb04b5603bc53cadd5c27d02cec4cbe
|
|
7
|
+
data.tar.gz: 28a6d1d21d23f0ba91d034b441ee7e858fc51d1d1662e9445b58ac215e09dd271871f90941121d04566e259d971cf6d615c56412ccf2cd9f08e5658e1dd20f0f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.21] - 2026-04-01
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- `ErrorTracer.record_trace` now dispatches `Trace.shared_store` writes/flushes in a background `Thread.new` — error/fatal logging hooks no longer block the calling Puma thread, regardless of store backend (Postgres, CacheStore, or in-memory)
|
|
7
|
+
- Debounce check remains synchronous; only the store write and flush go async
|
|
8
|
+
|
|
9
|
+
## [0.1.20] - 2026-03-31
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- CommunicationPattern sub-module for Phase C relational intelligence
|
|
13
|
+
- PatternAnalyzer: time-of-day/day-of-week histograms, channel preference, topic clustering, consistency scoring
|
|
14
|
+
- CommunicationPattern runner: update_patterns, analyze_patterns, pattern_stats
|
|
15
|
+
- Apollo Local persistence for communication pattern state
|
|
16
|
+
|
|
3
17
|
## [0.1.19] - 2026-03-31
|
|
4
18
|
|
|
5
19
|
### Fixed
|
data/Gemfile
CHANGED
data/lex-agentic-memory.gemspec
CHANGED
|
@@ -34,7 +34,7 @@ Gem::Specification.new do |spec|
|
|
|
34
34
|
spec.add_dependency 'msgpack', '~> 1.7'
|
|
35
35
|
|
|
36
36
|
spec.add_development_dependency 'rspec', '~> 3.13'
|
|
37
|
-
spec.add_development_dependency 'rubocop'
|
|
38
|
-
spec.add_development_dependency 'rubocop-legion'
|
|
39
|
-
spec.add_development_dependency 'rubocop-rspec'
|
|
37
|
+
spec.add_development_dependency 'rubocop'
|
|
38
|
+
spec.add_development_dependency 'rubocop-legion'
|
|
39
|
+
spec.add_development_dependency 'rubocop-rspec'
|
|
40
40
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Agentic
|
|
6
|
+
module Memory
|
|
7
|
+
module CommunicationPattern
|
|
8
|
+
module Helpers
|
|
9
|
+
module Constants
|
|
10
|
+
HOURS_IN_DAY = 24
|
|
11
|
+
DAYS_IN_WEEK = 7
|
|
12
|
+
|
|
13
|
+
MESSAGE_LENGTH_BUCKETS = %i[short medium long].freeze
|
|
14
|
+
MESSAGE_LENGTH_THRESHOLDS = { short: 50, medium: 200 }.freeze
|
|
15
|
+
|
|
16
|
+
SLIDING_WINDOW_SIZE = 100
|
|
17
|
+
MIN_TRACES_FOR_PATTERN = 10
|
|
18
|
+
|
|
19
|
+
TAG_PREFIX = %w[bond communication_pattern].freeze
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Agentic
|
|
6
|
+
module Memory
|
|
7
|
+
module CommunicationPattern
|
|
8
|
+
module Helpers
|
|
9
|
+
class PatternAnalyzer
|
|
10
|
+
attr_reader :agent_id, :trace_count
|
|
11
|
+
|
|
12
|
+
def initialize(agent_id:)
|
|
13
|
+
@agent_id = agent_id
|
|
14
|
+
@tod_histogram = Array.new(Constants::HOURS_IN_DAY, 0)
|
|
15
|
+
@dow_histogram = Array.new(Constants::DAYS_IN_WEEK, 0)
|
|
16
|
+
@channel_counts = Hash.new(0)
|
|
17
|
+
@direct_count = 0
|
|
18
|
+
@topic_counts = Hash.new(0)
|
|
19
|
+
@trace_count = 0
|
|
20
|
+
@dirty = false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def update_from_traces(traces)
|
|
24
|
+
traces.each { |t| process_trace(t) }
|
|
25
|
+
@dirty = true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def time_of_day_distribution
|
|
29
|
+
@tod_histogram.dup
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def day_of_week_distribution
|
|
33
|
+
@dow_histogram.dup
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def channel_preference
|
|
37
|
+
@channel_counts.sort_by { |_k, v| -v }.map(&:first)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def direct_address_frequency
|
|
41
|
+
return 0.0 if @trace_count.zero?
|
|
42
|
+
|
|
43
|
+
@direct_count.to_f / @trace_count
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def topic_clustering
|
|
47
|
+
@topic_counts.dup
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def consistency
|
|
51
|
+
return 0.0 if @trace_count < Constants::MIN_TRACES_FOR_PATTERN
|
|
52
|
+
|
|
53
|
+
channel_entropy = normalized_entropy(@channel_counts.values)
|
|
54
|
+
tod_entropy = normalized_entropy(@tod_histogram)
|
|
55
|
+
(1.0 - ((channel_entropy + tod_entropy) / 2.0)).clamp(0.0, 1.0)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def dirty?
|
|
59
|
+
@dirty
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def mark_clean!
|
|
63
|
+
@dirty = false
|
|
64
|
+
self
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def to_apollo_entries
|
|
68
|
+
tags = Constants::TAG_PREFIX.dup + [@agent_id]
|
|
69
|
+
[{ content: serialize(state_hash), tags: tags }]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def from_apollo(store:)
|
|
73
|
+
result = store.query(text: 'communication_pattern',
|
|
74
|
+
tags: Constants::TAG_PREFIX + [@agent_id])
|
|
75
|
+
return false unless result[:success] && result[:results]&.any?
|
|
76
|
+
|
|
77
|
+
parsed = deserialize(result[:results].first[:content])
|
|
78
|
+
return false unless parsed
|
|
79
|
+
|
|
80
|
+
restore_state(parsed)
|
|
81
|
+
true
|
|
82
|
+
rescue StandardError => e
|
|
83
|
+
warn "[pattern_analyzer] from_apollo error: #{e.message}"
|
|
84
|
+
false
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def process_trace(trace)
|
|
90
|
+
ts = trace[:created_at]
|
|
91
|
+
ts = Time.parse(ts.to_s) unless ts.is_a?(Time)
|
|
92
|
+
|
|
93
|
+
@tod_histogram[ts.hour] += 1
|
|
94
|
+
@dow_histogram[ts.wday] += 1
|
|
95
|
+
|
|
96
|
+
payload = trace[:content_payload] || {}
|
|
97
|
+
payload = payload.transform_keys(&:to_sym) if payload.is_a?(Hash)
|
|
98
|
+
|
|
99
|
+
channel = payload[:channel]&.to_s
|
|
100
|
+
@channel_counts[channel] += 1 if channel
|
|
101
|
+
|
|
102
|
+
@direct_count += 1 if payload[:direct_address]
|
|
103
|
+
|
|
104
|
+
(trace[:domain_tags] || []).each { |tag| @topic_counts[tag.to_s] += 1 }
|
|
105
|
+
|
|
106
|
+
@trace_count += 1
|
|
107
|
+
rescue StandardError => e
|
|
108
|
+
Legion::Logging.warn("[pattern_analyzer] process_trace error: #{e.message}")
|
|
109
|
+
nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def normalized_entropy(counts)
|
|
113
|
+
total = counts.sum.to_f
|
|
114
|
+
return 0.0 if total.zero?
|
|
115
|
+
|
|
116
|
+
probs = counts.select(&:positive?).map { |c| c / total }
|
|
117
|
+
max_entropy = Math.log(probs.size)
|
|
118
|
+
return 0.0 if max_entropy.zero?
|
|
119
|
+
|
|
120
|
+
entropy = -probs.sum { |p| p * Math.log(p) }
|
|
121
|
+
entropy / max_entropy
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def state_hash
|
|
125
|
+
{ agent_id: @agent_id, trace_count: @trace_count,
|
|
126
|
+
tod_histogram: @tod_histogram, dow_histogram: @dow_histogram,
|
|
127
|
+
channel_counts: @channel_counts, direct_count: @direct_count,
|
|
128
|
+
topic_counts: @topic_counts, consistency: consistency }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def restore_state(parsed)
|
|
132
|
+
@trace_count = parsed[:trace_count].to_i
|
|
133
|
+
@tod_histogram = parsed[:tod_histogram] || Array.new(24, 0)
|
|
134
|
+
@dow_histogram = parsed[:dow_histogram] || Array.new(7, 0)
|
|
135
|
+
@channel_counts = Hash.new(0).merge(parsed[:channel_counts] || {})
|
|
136
|
+
@direct_count = parsed[:direct_count].to_i
|
|
137
|
+
@topic_counts = Hash.new(0).merge(parsed[:topic_counts] || {})
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def serialize(hash)
|
|
141
|
+
defined?(Legion::JSON) ? Legion::JSON.dump(hash) : ::JSON.dump(hash)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def deserialize(content)
|
|
145
|
+
parsed = defined?(Legion::JSON) ? Legion::JSON.parse(content) : ::JSON.parse(content, symbolize_names: true)
|
|
146
|
+
parsed.is_a?(Hash) ? parsed.transform_keys(&:to_sym) : nil
|
|
147
|
+
rescue StandardError => e
|
|
148
|
+
Legion::Logging.warn("[pattern_analyzer] deserialize error: #{e.message}")
|
|
149
|
+
nil
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
data/lib/legion/extensions/agentic/memory/communication_pattern/runners/communication_pattern.rb
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Agentic
|
|
6
|
+
module Memory
|
|
7
|
+
module CommunicationPattern
|
|
8
|
+
module Runners
|
|
9
|
+
module CommunicationPattern
|
|
10
|
+
include Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers) &&
|
|
11
|
+
Legion::Extensions::Helpers.const_defined?(:Lex, false)
|
|
12
|
+
|
|
13
|
+
def update_patterns(agent_id:, traces: [], **)
|
|
14
|
+
analyzer = analyzer_for(agent_id)
|
|
15
|
+
analyzer.update_from_traces(traces)
|
|
16
|
+
|
|
17
|
+
{ success: true, trace_count: analyzer.trace_count,
|
|
18
|
+
channel_preference: analyzer.channel_preference,
|
|
19
|
+
direct_address_frequency: analyzer.direct_address_frequency,
|
|
20
|
+
consistency: analyzer.consistency }
|
|
21
|
+
rescue StandardError => e
|
|
22
|
+
{ success: false, error: e.message }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def analyze_patterns(agent_id:, **)
|
|
26
|
+
analyzer = analyzer_for(agent_id)
|
|
27
|
+
{ time_of_day_distribution: analyzer.time_of_day_distribution,
|
|
28
|
+
day_of_week_distribution: analyzer.day_of_week_distribution,
|
|
29
|
+
channel_preference: analyzer.channel_preference,
|
|
30
|
+
direct_address_frequency: analyzer.direct_address_frequency,
|
|
31
|
+
topic_clustering: analyzer.topic_clustering,
|
|
32
|
+
consistency: analyzer.consistency,
|
|
33
|
+
trace_count: analyzer.trace_count }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def pattern_stats(agent_id:, **)
|
|
37
|
+
analyzer = analyzer_for(agent_id)
|
|
38
|
+
{ trace_count: analyzer.trace_count,
|
|
39
|
+
channel_preference: analyzer.channel_preference,
|
|
40
|
+
consistency: analyzer.consistency }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def analyzer_for(agent_id)
|
|
46
|
+
@analyzers ||= {}
|
|
47
|
+
@analyzers[agent_id.to_s] ||= Helpers::PatternAnalyzer.new(agent_id: agent_id.to_s)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'communication_pattern/version'
|
|
4
|
+
require_relative 'communication_pattern/helpers/constants'
|
|
5
|
+
require_relative 'communication_pattern/helpers/pattern_analyzer'
|
|
6
|
+
require_relative 'communication_pattern/runners/communication_pattern'
|
|
7
|
+
require_relative 'communication_pattern/client'
|
|
@@ -17,8 +17,12 @@ module Legion
|
|
|
17
17
|
def setup
|
|
18
18
|
return if @active
|
|
19
19
|
|
|
20
|
-
@recent
|
|
21
|
-
@
|
|
20
|
+
@recent = {}
|
|
21
|
+
@recent_mutex = ::Mutex.new
|
|
22
|
+
@runner = Object.new.extend(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces)
|
|
23
|
+
@write_queue = ::Queue.new
|
|
24
|
+
@worker = ::Thread.new { drain_queue }
|
|
25
|
+
@worker.name = 'legion-error-tracer'
|
|
22
26
|
wrap_logging_methods
|
|
23
27
|
@active = true
|
|
24
28
|
Legion::Logging.info '[memory] ErrorTracer active — errors/fatals will become episodic traces'
|
|
@@ -30,6 +34,19 @@ module Legion
|
|
|
30
34
|
|
|
31
35
|
private
|
|
32
36
|
|
|
37
|
+
def drain_queue
|
|
38
|
+
loop do
|
|
39
|
+
payload = @write_queue.pop
|
|
40
|
+
break if payload == :stop
|
|
41
|
+
|
|
42
|
+
@runner.store_trace(**payload)
|
|
43
|
+
store = @runner.send(:default_store)
|
|
44
|
+
store.flush if store.respond_to?(:flush)
|
|
45
|
+
rescue StandardError
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
33
50
|
def wrap_logging_methods
|
|
34
51
|
original_error = Legion::Logging.method(:error)
|
|
35
52
|
original_fatal = Legion::Logging.method(:fatal)
|
|
@@ -50,23 +67,21 @@ module Legion
|
|
|
50
67
|
def record_trace(message, level)
|
|
51
68
|
return unless message.is_a?(String) && !message.empty?
|
|
52
69
|
|
|
53
|
-
# Debounce: skip if same message within window
|
|
54
70
|
now = Time.now.utc
|
|
55
71
|
key = "#{level}:#{message[0..100]}"
|
|
56
|
-
return if @recent[key] && (now - @recent[key]) < DEBOUNCE_WINDOW
|
|
57
72
|
|
|
58
|
-
@
|
|
73
|
+
@recent_mutex.synchronize do
|
|
74
|
+
return if @recent[key] && (now - @recent[key]) < DEBOUNCE_WINDOW
|
|
59
75
|
|
|
60
|
-
|
|
61
|
-
|
|
76
|
+
@recent[key] = now
|
|
77
|
+
@recent.delete_if { |_, t| (now - t) > DEBOUNCE_WINDOW } if @recent.size > 500
|
|
78
|
+
end
|
|
62
79
|
|
|
63
|
-
# Extract component from [bracket] prefix
|
|
64
80
|
component = message.match(/\A\[([^\]]+)\]/)&.captures&.first || 'unknown'
|
|
65
|
-
|
|
66
81
|
valence = level == :fatal ? FATAL_VALENCE : ERROR_VALENCE
|
|
67
82
|
intensity = level == :fatal ? FATAL_INTENSITY : ERROR_INTENSITY
|
|
68
83
|
|
|
69
|
-
@
|
|
84
|
+
@write_queue.push(
|
|
70
85
|
type: :episodic,
|
|
71
86
|
content_payload: message,
|
|
72
87
|
domain_tags: ['error', component.downcase],
|
|
@@ -76,12 +91,7 @@ module Legion
|
|
|
76
91
|
unresolved: true,
|
|
77
92
|
confidence: 0.9
|
|
78
93
|
)
|
|
79
|
-
|
|
80
|
-
# Flush if cache-backed
|
|
81
|
-
store = @runner.send(:default_store)
|
|
82
|
-
store.flush if store.respond_to?(:flush)
|
|
83
|
-
rescue StandardError => _e
|
|
84
|
-
# Never let trace creation break the logging pipeline
|
|
94
|
+
rescue StandardError
|
|
85
95
|
nil
|
|
86
96
|
end
|
|
87
97
|
end
|
|
@@ -19,6 +19,7 @@ require_relative 'memory/semantic_priming'
|
|
|
19
19
|
require_relative 'memory/semantic_satiation'
|
|
20
20
|
require_relative 'memory/source_monitoring'
|
|
21
21
|
require_relative 'memory/transfer'
|
|
22
|
+
require_relative 'memory/communication_pattern'
|
|
22
23
|
|
|
23
24
|
module Legion
|
|
24
25
|
module Extensions
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'legion/extensions/agentic/memory/communication_pattern/helpers/constants'
|
|
5
|
+
|
|
6
|
+
RSpec.describe Legion::Extensions::Agentic::Memory::CommunicationPattern::Helpers::Constants do
|
|
7
|
+
describe 'HOURS_IN_DAY' do
|
|
8
|
+
it 'is 24' do
|
|
9
|
+
expect(described_class::HOURS_IN_DAY).to eq(24)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
describe 'DAYS_IN_WEEK' do
|
|
14
|
+
it 'is 7' do
|
|
15
|
+
expect(described_class::DAYS_IN_WEEK).to eq(7)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe 'MESSAGE_LENGTH_BUCKETS' do
|
|
20
|
+
it 'defines 3 buckets' do
|
|
21
|
+
expect(described_class::MESSAGE_LENGTH_BUCKETS).to eq(%i[short medium long])
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
describe 'SLIDING_WINDOW_SIZE' do
|
|
26
|
+
it 'is 100' do
|
|
27
|
+
expect(described_class::SLIDING_WINDOW_SIZE).to eq(100)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe 'MIN_TRACES_FOR_PATTERN' do
|
|
32
|
+
it 'is 10' do
|
|
33
|
+
expect(described_class::MIN_TRACES_FOR_PATTERN).to eq(10)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/spec/legion/extensions/agentic/memory/communication_pattern/helpers/pattern_analyzer_spec.rb
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'legion/extensions/agentic/memory/communication_pattern/helpers/constants'
|
|
5
|
+
require 'legion/extensions/agentic/memory/communication_pattern/helpers/pattern_analyzer'
|
|
6
|
+
|
|
7
|
+
RSpec.describe Legion::Extensions::Agentic::Memory::CommunicationPattern::Helpers::PatternAnalyzer do
|
|
8
|
+
subject(:analyzer) { described_class.new(agent_id: 'partner-1') }
|
|
9
|
+
|
|
10
|
+
let(:trace_9am) do
|
|
11
|
+
{ trace_id: 'tr1', trace_type: :episodic, created_at: Time.utc(2026, 3, 31, 9, 0),
|
|
12
|
+
content_payload: { channel: 'teams', direct_address: true, agent_id: 'partner-1' },
|
|
13
|
+
domain_tags: %w[partner observation], source_agent_id: 'partner-1' }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
let(:trace_2pm) do
|
|
17
|
+
{ trace_id: 'tr2', trace_type: :episodic, created_at: Time.utc(2026, 3, 31, 14, 0),
|
|
18
|
+
content_payload: { channel: 'cli', direct_address: false, agent_id: 'partner-1' },
|
|
19
|
+
domain_tags: %w[partner observation work], source_agent_id: 'partner-1' }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe '#update_from_traces' do
|
|
23
|
+
it 'processes an array of traces' do
|
|
24
|
+
analyzer.update_from_traces([trace_9am, trace_2pm])
|
|
25
|
+
expect(analyzer.trace_count).to eq(2)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'marks dirty' do
|
|
29
|
+
analyzer.update_from_traces([trace_9am])
|
|
30
|
+
expect(analyzer).to be_dirty
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe '#time_of_day_distribution' do
|
|
35
|
+
before { analyzer.update_from_traces([trace_9am, trace_2pm]) }
|
|
36
|
+
|
|
37
|
+
it 'returns 24-bucket histogram' do
|
|
38
|
+
dist = analyzer.time_of_day_distribution
|
|
39
|
+
expect(dist.size).to eq(24)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'has counts in the right buckets' do
|
|
43
|
+
dist = analyzer.time_of_day_distribution
|
|
44
|
+
expect(dist[9]).to eq(1)
|
|
45
|
+
expect(dist[14]).to eq(1)
|
|
46
|
+
expect(dist[0]).to eq(0)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe '#day_of_week_distribution' do
|
|
51
|
+
before { analyzer.update_from_traces([trace_9am]) }
|
|
52
|
+
|
|
53
|
+
it 'returns 7-bucket histogram' do
|
|
54
|
+
dist = analyzer.day_of_week_distribution
|
|
55
|
+
expect(dist.size).to eq(7)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
describe '#channel_preference' do
|
|
60
|
+
before { analyzer.update_from_traces([trace_9am, trace_2pm, trace_9am.merge(trace_id: 'tr3')]) }
|
|
61
|
+
|
|
62
|
+
it 'ranks channels by frequency' do
|
|
63
|
+
prefs = analyzer.channel_preference
|
|
64
|
+
expect(prefs.first).to eq('teams')
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe '#direct_address_frequency' do
|
|
69
|
+
before { analyzer.update_from_traces([trace_9am, trace_2pm]) }
|
|
70
|
+
|
|
71
|
+
it 'computes ratio of direct address traces' do
|
|
72
|
+
ratio = analyzer.direct_address_frequency
|
|
73
|
+
expect(ratio).to be_within(0.01).of(0.5)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
describe '#topic_clustering' do
|
|
78
|
+
before { analyzer.update_from_traces([trace_9am, trace_2pm]) }
|
|
79
|
+
|
|
80
|
+
it 'returns domain tag frequencies' do
|
|
81
|
+
topics = analyzer.topic_clustering
|
|
82
|
+
expect(topics).to have_key('partner')
|
|
83
|
+
expect(topics['partner']).to eq(2)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
describe '#consistency' do
|
|
88
|
+
it 'returns 0.0 with no data' do
|
|
89
|
+
expect(analyzer.consistency).to eq(0.0)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'returns positive value with data' do
|
|
93
|
+
analyzer.update_from_traces([trace_9am, trace_2pm])
|
|
94
|
+
expect(analyzer.consistency).to eq(0.0)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
describe 'Apollo persistence' do
|
|
99
|
+
describe '#to_apollo_entries' do
|
|
100
|
+
before { analyzer.update_from_traces([trace_9am]) }
|
|
101
|
+
|
|
102
|
+
it 'returns entries with correct tags' do
|
|
103
|
+
entries = analyzer.to_apollo_entries
|
|
104
|
+
expect(entries.first[:tags]).to include('bond', 'communication_pattern', 'partner-1')
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
describe '#from_apollo' do
|
|
109
|
+
let(:mock_store) { double('apollo_local') }
|
|
110
|
+
|
|
111
|
+
it 'restores state' do
|
|
112
|
+
analyzer.update_from_traces([trace_9am, trace_2pm])
|
|
113
|
+
content = analyzer.send(:serialize, analyzer.send(:state_hash))
|
|
114
|
+
|
|
115
|
+
new_analyzer = described_class.new(agent_id: 'partner-1')
|
|
116
|
+
allow(mock_store).to receive(:query)
|
|
117
|
+
.and_return({ success: true, results: [{ content: content }] })
|
|
118
|
+
|
|
119
|
+
expect(new_analyzer.from_apollo(store: mock_store)).to be true
|
|
120
|
+
expect(new_analyzer.trace_count).to eq(2)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
describe '#mark_clean!' do
|
|
125
|
+
it 'clears dirty flag' do
|
|
126
|
+
analyzer.update_from_traces([trace_9am])
|
|
127
|
+
analyzer.mark_clean!
|
|
128
|
+
expect(analyzer).not_to be_dirty
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'legion/extensions/agentic/memory/communication_pattern/helpers/constants'
|
|
5
|
+
require 'legion/extensions/agentic/memory/communication_pattern/helpers/pattern_analyzer'
|
|
6
|
+
require 'legion/extensions/agentic/memory/communication_pattern/runners/communication_pattern'
|
|
7
|
+
|
|
8
|
+
RSpec.describe Legion::Extensions::Agentic::Memory::CommunicationPattern::Runners::CommunicationPattern do
|
|
9
|
+
let(:host) { Object.new.extend(described_module) }
|
|
10
|
+
let(:described_module) { described_class }
|
|
11
|
+
|
|
12
|
+
before { host.instance_variable_set(:@analyzers, nil) }
|
|
13
|
+
|
|
14
|
+
describe '#update_patterns' do
|
|
15
|
+
let(:traces) do
|
|
16
|
+
[
|
|
17
|
+
{ trace_id: 'tr1', trace_type: :episodic, created_at: Time.utc(2026, 3, 31, 9, 0),
|
|
18
|
+
content_payload: { channel: 'teams', direct_address: true }, domain_tags: %w[partner],
|
|
19
|
+
source_agent_id: 'partner-1' },
|
|
20
|
+
{ trace_id: 'tr2', trace_type: :episodic, created_at: Time.utc(2026, 3, 31, 14, 0),
|
|
21
|
+
content_payload: { channel: 'cli', direct_address: false }, domain_tags: %w[partner],
|
|
22
|
+
source_agent_id: 'partner-1' }
|
|
23
|
+
]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'processes traces and returns result' do
|
|
27
|
+
result = host.update_patterns(agent_id: 'partner-1', traces: traces)
|
|
28
|
+
expect(result[:success]).to be true
|
|
29
|
+
expect(result[:trace_count]).to eq(2)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'returns channel preference' do
|
|
33
|
+
result = host.update_patterns(agent_id: 'partner-1', traces: traces)
|
|
34
|
+
expect(result[:channel_preference]).to be_an(Array)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'returns direct address frequency' do
|
|
38
|
+
result = host.update_patterns(agent_id: 'partner-1', traces: traces)
|
|
39
|
+
expect(result[:direct_address_frequency]).to be_within(0.01).of(0.5)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe '#analyze_patterns' do
|
|
44
|
+
it 'returns current patterns for an agent' do
|
|
45
|
+
host.update_patterns(agent_id: 'p1', traces: [
|
|
46
|
+
{ trace_id: 't1', trace_type: :episodic, created_at: Time.now.utc,
|
|
47
|
+
content_payload: { channel: 'teams' }, domain_tags: [], source_agent_id: 'p1' }
|
|
48
|
+
])
|
|
49
|
+
result = host.analyze_patterns(agent_id: 'p1')
|
|
50
|
+
expect(result).to have_key(:time_of_day_distribution)
|
|
51
|
+
expect(result).to have_key(:channel_preference)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'returns empty result for unknown agent' do
|
|
55
|
+
result = host.analyze_patterns(agent_id: 'unknown')
|
|
56
|
+
expect(result[:trace_count]).to eq(0)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
describe '#pattern_stats' do
|
|
61
|
+
it 'returns stats hash' do
|
|
62
|
+
result = host.pattern_stats(agent_id: 'p1')
|
|
63
|
+
expect(result).to have_key(:trace_count)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -11,13 +11,24 @@ RSpec.describe Legion::Extensions::Agentic::Memory::Trace::Helpers::ErrorTracer
|
|
|
11
11
|
# Reset state between examples
|
|
12
12
|
described_class.instance_variable_set(:@active, nil)
|
|
13
13
|
described_class.instance_variable_set(:@recent, nil)
|
|
14
|
+
described_class.instance_variable_set(:@recent_mutex, nil)
|
|
14
15
|
described_class.instance_variable_set(:@runner, nil)
|
|
16
|
+
described_class.instance_variable_set(:@write_queue, nil)
|
|
17
|
+
old_worker = described_class.instance_variable_get(:@worker)
|
|
18
|
+
old_worker&.kill
|
|
19
|
+
described_class.instance_variable_set(:@worker, nil)
|
|
15
20
|
end
|
|
16
21
|
|
|
17
22
|
after do
|
|
18
23
|
# Restore logging singleton methods to prevent cross-test side effects
|
|
19
24
|
Legion::Logging.define_singleton_method(:error, &original_error)
|
|
20
25
|
Legion::Logging.define_singleton_method(:fatal, &original_fatal)
|
|
26
|
+
# Stop worker thread if still alive
|
|
27
|
+
worker = described_class.instance_variable_get(:@worker)
|
|
28
|
+
if worker&.alive?
|
|
29
|
+
described_class.instance_variable_get(:@write_queue)&.push(:stop)
|
|
30
|
+
worker.join(1)
|
|
31
|
+
end
|
|
21
32
|
end
|
|
22
33
|
|
|
23
34
|
describe '.setup' do
|
|
@@ -31,6 +42,13 @@ RSpec.describe Legion::Extensions::Agentic::Memory::Trace::Helpers::ErrorTracer
|
|
|
31
42
|
described_class.setup
|
|
32
43
|
expect(described_class.active?).to be true
|
|
33
44
|
end
|
|
45
|
+
|
|
46
|
+
it 'starts a single background worker thread' do
|
|
47
|
+
described_class.setup
|
|
48
|
+
worker = described_class.instance_variable_get(:@worker)
|
|
49
|
+
expect(worker).to be_a(Thread)
|
|
50
|
+
expect(worker).to be_alive
|
|
51
|
+
end
|
|
34
52
|
end
|
|
35
53
|
|
|
36
54
|
describe '.active?' do
|
|
@@ -43,4 +61,114 @@ RSpec.describe Legion::Extensions::Agentic::Memory::Trace::Helpers::ErrorTracer
|
|
|
43
61
|
expect(described_class.active?).to be true
|
|
44
62
|
end
|
|
45
63
|
end
|
|
64
|
+
|
|
65
|
+
describe 'record_trace (async dispatch via queue)' do
|
|
66
|
+
let(:store_dbl) { double('store', flush: nil) }
|
|
67
|
+
let(:runner_double) do
|
|
68
|
+
dbl = double('runner')
|
|
69
|
+
allow(dbl).to receive(:store_trace)
|
|
70
|
+
allow(dbl).to receive(:default_store).and_return(store_dbl)
|
|
71
|
+
dbl
|
|
72
|
+
end
|
|
73
|
+
let(:write_queue) { Queue.new }
|
|
74
|
+
|
|
75
|
+
before do
|
|
76
|
+
described_class.instance_variable_set(:@active, true)
|
|
77
|
+
described_class.instance_variable_set(:@recent, {})
|
|
78
|
+
described_class.instance_variable_set(:@recent_mutex, Mutex.new)
|
|
79
|
+
described_class.instance_variable_set(:@runner, runner_double)
|
|
80
|
+
described_class.instance_variable_set(:@write_queue, write_queue)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it 'enqueues a payload hash (fire-and-forget dispatch)' do
|
|
84
|
+
described_class.send(:record_trace, 'something broke', :error)
|
|
85
|
+
expect(write_queue.size).to eq(1)
|
|
86
|
+
payload = write_queue.pop
|
|
87
|
+
expect(payload).to be_a(Hash)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'enqueues a payload with expected keys' do
|
|
91
|
+
described_class.send(:record_trace, '[mycomponent] disk full', :error)
|
|
92
|
+
payload = write_queue.pop
|
|
93
|
+
expect(payload).to include(
|
|
94
|
+
type: :episodic,
|
|
95
|
+
content_payload: '[mycomponent] disk full',
|
|
96
|
+
domain_tags: %w[error mycomponent],
|
|
97
|
+
unresolved: true
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'uses fatal valence and intensity for :fatal level' do
|
|
102
|
+
described_class.send(:record_trace, 'total meltdown', :fatal)
|
|
103
|
+
payload = write_queue.pop
|
|
104
|
+
expect(payload).to include(
|
|
105
|
+
emotional_valence: Legion::Extensions::Agentic::Memory::Trace::Helpers::ErrorTracer::FATAL_VALENCE,
|
|
106
|
+
emotional_intensity: Legion::Extensions::Agentic::Memory::Trace::Helpers::ErrorTracer::FATAL_INTENSITY
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it 'does not enqueue (debounced) when same message is within the window' do
|
|
111
|
+
described_class.send(:record_trace, 'repeated error', :error)
|
|
112
|
+
described_class.send(:record_trace, 'repeated error', :error)
|
|
113
|
+
expect(write_queue.size).to eq(1)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it 'returns nil for blank messages without raising' do
|
|
117
|
+
expect(described_class.send(:record_trace, '', :error)).to be_nil
|
|
118
|
+
expect(write_queue.size).to eq(0)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it 'does not propagate errors from store_trace' do
|
|
122
|
+
worker_queue = Queue.new
|
|
123
|
+
described_class.instance_variable_set(:@write_queue, worker_queue)
|
|
124
|
+
allow(runner_double).to receive(:store_trace).and_raise(StandardError, 'db down')
|
|
125
|
+
|
|
126
|
+
# rubocop:disable ThreadSafety/NewThread
|
|
127
|
+
worker = Thread.new do
|
|
128
|
+
payload = worker_queue.pop
|
|
129
|
+
break if payload == :stop
|
|
130
|
+
|
|
131
|
+
runner_double.store_trace(**payload)
|
|
132
|
+
rescue StandardError
|
|
133
|
+
nil
|
|
134
|
+
end
|
|
135
|
+
# rubocop:enable ThreadSafety/NewThread
|
|
136
|
+
|
|
137
|
+
described_class.send(:record_trace, 'store failure', :error)
|
|
138
|
+
expect { worker.join(2) }.not_to raise_error
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it 'the background worker calls store_trace with the enqueued payload' do
|
|
142
|
+
dedicated_queue = Queue.new
|
|
143
|
+
described_class.instance_variable_set(:@write_queue, dedicated_queue)
|
|
144
|
+
|
|
145
|
+
# rubocop:disable ThreadSafety/NewThread
|
|
146
|
+
worker = Thread.new do
|
|
147
|
+
loop do
|
|
148
|
+
payload = dedicated_queue.pop
|
|
149
|
+
break if payload == :stop
|
|
150
|
+
|
|
151
|
+
runner_double.store_trace(**payload)
|
|
152
|
+
store = runner_double.send(:default_store)
|
|
153
|
+
store.flush if store.respond_to?(:flush)
|
|
154
|
+
rescue StandardError
|
|
155
|
+
nil
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
# rubocop:enable ThreadSafety/NewThread
|
|
159
|
+
|
|
160
|
+
described_class.send(:record_trace, '[mycomponent] disk full', :error)
|
|
161
|
+
dedicated_queue.push(:stop)
|
|
162
|
+
worker.join(2)
|
|
163
|
+
|
|
164
|
+
expect(runner_double).to have_received(:store_trace).with(
|
|
165
|
+
hash_including(
|
|
166
|
+
type: :episodic,
|
|
167
|
+
content_payload: '[mycomponent] disk full',
|
|
168
|
+
domain_tags: %w[error mycomponent],
|
|
169
|
+
unresolved: true
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
46
174
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-agentic-memory
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.21
|
|
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: '
|
|
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: '
|
|
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
|
|
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
|
|
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: '
|
|
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: '
|
|
179
|
+
version: '0'
|
|
180
180
|
description: 'LEX agentic memory domain: episodic, semantic, and working memory'
|
|
181
181
|
email:
|
|
182
182
|
- matthewdiverson@gmail.com
|
|
@@ -199,6 +199,12 @@ files:
|
|
|
199
199
|
- lib/legion/extensions/agentic/memory/archaeology/helpers/excavation_site.rb
|
|
200
200
|
- lib/legion/extensions/agentic/memory/archaeology/runners/cognitive_archaeology.rb
|
|
201
201
|
- lib/legion/extensions/agentic/memory/archaeology/version.rb
|
|
202
|
+
- lib/legion/extensions/agentic/memory/communication_pattern.rb
|
|
203
|
+
- lib/legion/extensions/agentic/memory/communication_pattern/client.rb
|
|
204
|
+
- lib/legion/extensions/agentic/memory/communication_pattern/helpers/constants.rb
|
|
205
|
+
- lib/legion/extensions/agentic/memory/communication_pattern/helpers/pattern_analyzer.rb
|
|
206
|
+
- lib/legion/extensions/agentic/memory/communication_pattern/runners/communication_pattern.rb
|
|
207
|
+
- lib/legion/extensions/agentic/memory/communication_pattern/version.rb
|
|
202
208
|
- lib/legion/extensions/agentic/memory/compression.rb
|
|
203
209
|
- lib/legion/extensions/agentic/memory/compression/actors/maintenance.rb
|
|
204
210
|
- lib/legion/extensions/agentic/memory/compression/client.rb
|
|
@@ -363,6 +369,9 @@ files:
|
|
|
363
369
|
- spec/legion/extensions/agentic/memory/archaeology/helpers/constants_spec.rb
|
|
364
370
|
- spec/legion/extensions/agentic/memory/archaeology/helpers/excavation_site_spec.rb
|
|
365
371
|
- spec/legion/extensions/agentic/memory/archaeology/runners/cognitive_archaeology_spec.rb
|
|
372
|
+
- spec/legion/extensions/agentic/memory/communication_pattern/helpers/constants_spec.rb
|
|
373
|
+
- spec/legion/extensions/agentic/memory/communication_pattern/helpers/pattern_analyzer_spec.rb
|
|
374
|
+
- spec/legion/extensions/agentic/memory/communication_pattern/runners/communication_pattern_spec.rb
|
|
366
375
|
- spec/legion/extensions/agentic/memory/compression/actors/maintenance_spec.rb
|
|
367
376
|
- spec/legion/extensions/agentic/memory/compression/helpers/compression_engine_spec.rb
|
|
368
377
|
- spec/legion/extensions/agentic/memory/compression/helpers/constants_spec.rb
|