engram 0.4.0 → 0.5.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 +4 -4
- data/CHANGELOG.md +57 -0
- data/README.md +95 -9
- data/lib/engram/adapters/in_memory_processed_turns.rb +40 -8
- data/lib/engram/adapters/in_memory_store.rb +25 -10
- data/lib/engram/adapters/null_embedder.rb +9 -0
- data/lib/engram/adapters/pgvector_store.rb +35 -17
- data/lib/engram/adapters/ruby_llm_embedder.rb +45 -4
- data/lib/engram/consolidators/heuristic_consolidator.rb +7 -1
- data/lib/engram/consolidators/llm_consolidator.rb +37 -10
- data/lib/engram/embedding_metadata.rb +135 -0
- data/lib/engram/extractors/llm_extractor.rb +4 -3
- data/lib/engram/memory.rb +13 -1
- data/lib/engram/persistence.rb +10 -5
- data/lib/engram/ports/memory_store.rb +14 -7
- data/lib/engram/ports/processed_turns.rb +16 -8
- data/lib/engram/provenance.rb +257 -0
- data/lib/engram/rails/cache_processed_turns.rb +51 -10
- data/lib/engram/rails/observe_job.rb +5 -0
- data/lib/engram/rails/tasks.rake +26 -0
- data/lib/engram/railtie.rb +4 -0
- data/lib/engram/reserved_metadata.rb +52 -0
- data/lib/engram/use_cases/forget.rb +6 -2
- data/lib/engram/use_cases/observe.rb +43 -22
- data/lib/engram/use_cases/rebuild_embeddings.rb +189 -0
- data/lib/engram/use_cases/recall.rb +12 -4
- data/lib/engram/version.rb +1 -1
- data/lib/engram.rb +7 -0
- metadata +8 -3
data/lib/engram/railtie.rb
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Engram
|
|
4
|
+
# Collision-preserving operations for schemas sharing Engram's metadata namespace.
|
|
5
|
+
module ReservedMetadata
|
|
6
|
+
KEY = "_engram"
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def attach(metadata, schema_key, value, namespace_description: "metadata")
|
|
11
|
+
metadata = (metadata || {}).dup
|
|
12
|
+
reserved_values = [metadata.delete(KEY), metadata.delete(:_engram)].compact
|
|
13
|
+
unless reserved_values.all? { |reserved| reserved.is_a?(Hash) }
|
|
14
|
+
raise Engram::Error, "metadata key #{KEY.inspect} is reserved for Engram #{namespace_description}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
reserved = reserved_values.reduce({}) do |merged, reserved_value|
|
|
18
|
+
siblings = reserved_value.reject { |key, _nested| key.to_s == schema_key }
|
|
19
|
+
merge(merged, normalize(siblings))
|
|
20
|
+
end
|
|
21
|
+
metadata.merge(KEY => reserved.merge(schema_key => normalize(value)))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def normalize(value, path = [])
|
|
25
|
+
return value.map { |nested| normalize(nested, path) } if value.is_a?(Array)
|
|
26
|
+
return value unless value.is_a?(Hash)
|
|
27
|
+
|
|
28
|
+
value.each_with_object({}) do |(key, nested), normalized|
|
|
29
|
+
string_key = key.to_s
|
|
30
|
+
normalized_value = normalize(nested, path + [string_key])
|
|
31
|
+
normalized[string_key] = if normalized.key?(string_key)
|
|
32
|
+
merge_value(normalized[string_key], normalized_value, path + [string_key])
|
|
33
|
+
else
|
|
34
|
+
normalized_value
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def merge(left, right, path = [])
|
|
40
|
+
right.each_with_object(left.dup) do |(key, value), merged|
|
|
41
|
+
merged[key] = merged.key?(key) ? merge_value(merged[key], value, path + [key]) : value
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def merge_value(left, right, path)
|
|
46
|
+
return merge(left, right, path) if left.is_a?(Hash) && right.is_a?(Hash)
|
|
47
|
+
return left if left == right
|
|
48
|
+
|
|
49
|
+
raise Engram::Error, "conflicting reserved metadata at #{path.join(".")}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -20,8 +20,12 @@ module Engram
|
|
|
20
20
|
timestamp && timestamp < cutoff && record.importance.to_f < min_importance
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
stale.
|
|
24
|
-
|
|
23
|
+
stale.select do |record|
|
|
24
|
+
next false unless record.id
|
|
25
|
+
|
|
26
|
+
deleted = @store.delete(scope: scope, id: record.id)
|
|
27
|
+
deleted && deleted != 0
|
|
28
|
+
end
|
|
25
29
|
end
|
|
26
30
|
end
|
|
27
31
|
end
|
|
@@ -26,38 +26,54 @@ module Engram
|
|
|
26
26
|
idempotency_key_present: !idempotency_key.nil?
|
|
27
27
|
)
|
|
28
28
|
Engram::Instrumentation.instrument("observe", payload) do
|
|
29
|
-
|
|
29
|
+
claim = acquire_claim(scope, idempotency_key)
|
|
30
|
+
if idempotency_key && @processed_turns && !claim
|
|
31
|
+
unless @processed_turns.completed?(scope: scope, key: idempotency_key)
|
|
32
|
+
# A live, incomplete lease is retryable, not a completed duplicate.
|
|
33
|
+
raise Engram::ObservationInProgressError,
|
|
34
|
+
"observation for this turn is claimed but not completed; retry after the claim lease expires"
|
|
35
|
+
end
|
|
36
|
+
|
|
30
37
|
payload[:skipped] = true
|
|
31
38
|
payload[:candidate_count] = 0
|
|
32
39
|
payload[:decision_count] = 0
|
|
33
40
|
next []
|
|
34
41
|
end
|
|
35
42
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
begin
|
|
44
|
+
candidates = extract(messages: messages, scope: scope)
|
|
45
|
+
payload[:candidate_count] = candidates.size
|
|
46
|
+
if candidates.empty?
|
|
47
|
+
complete_claim(scope, idempotency_key, claim)
|
|
48
|
+
payload[:decision_count] = 0
|
|
49
|
+
next []
|
|
50
|
+
end
|
|
43
51
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
decisions = consolidate(candidates: candidates, scope: scope)
|
|
53
|
+
applied_decisions = decisions.filter_map { |decision| apply(decision, scope) }
|
|
54
|
+
payload[:decision_count] = applied_decisions.size
|
|
55
|
+
payload[:decision_actions] = applied_decisions.map { |decision| decision.action.to_s }
|
|
56
|
+
complete_claim(scope, idempotency_key, claim)
|
|
57
|
+
applied_decisions
|
|
58
|
+
rescue
|
|
59
|
+
release_claim(scope, idempotency_key, claim)
|
|
60
|
+
raise
|
|
61
|
+
end
|
|
50
62
|
end
|
|
51
63
|
end
|
|
52
64
|
|
|
53
65
|
private
|
|
54
66
|
|
|
55
|
-
def
|
|
56
|
-
|
|
67
|
+
def acquire_claim(scope, key)
|
|
68
|
+
@processed_turns.claim(scope: scope, key: key) if key && @processed_turns
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def complete_claim(scope, key, claim)
|
|
72
|
+
@processed_turns.complete(scope: scope, key: key, claim: claim) if claim
|
|
57
73
|
end
|
|
58
74
|
|
|
59
|
-
def
|
|
60
|
-
@processed_turns.
|
|
75
|
+
def release_claim(scope, key, claim)
|
|
76
|
+
@processed_turns.release(scope: scope, key: key, claim: claim) if claim
|
|
61
77
|
end
|
|
62
78
|
|
|
63
79
|
def extract(messages:, scope:)
|
|
@@ -79,16 +95,21 @@ module Engram
|
|
|
79
95
|
end
|
|
80
96
|
end
|
|
81
97
|
|
|
82
|
-
def apply(decision)
|
|
98
|
+
def apply(decision, scope)
|
|
83
99
|
case decision.action
|
|
84
100
|
when :add
|
|
85
|
-
|
|
101
|
+
raise Engram::Error, "cannot move memory across scopes" unless decision.candidate.scope == scope
|
|
102
|
+
|
|
103
|
+
decision if persistence.add(decision.candidate, scope: scope)
|
|
86
104
|
when :update
|
|
87
|
-
if decision.target_id && persistence.update(id: decision.target_id, record: decision.candidate)
|
|
105
|
+
if decision.target_id && persistence.update(scope: scope, id: decision.target_id, record: decision.candidate)
|
|
88
106
|
decision
|
|
89
107
|
end
|
|
90
108
|
when :forget
|
|
91
|
-
|
|
109
|
+
if decision.target_id
|
|
110
|
+
deleted = @store.delete(scope: scope, id: decision.target_id)
|
|
111
|
+
decision if deleted && deleted != 0
|
|
112
|
+
end
|
|
92
113
|
when :noop
|
|
93
114
|
nil
|
|
94
115
|
end
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Engram
|
|
4
|
+
module UseCases
|
|
5
|
+
# Rebuild stored embeddings when the active embedder configuration changes.
|
|
6
|
+
# Returns counts useful for logging/observability:
|
|
7
|
+
# - :processed: total rows examined in scope
|
|
8
|
+
# - :updated: rows re-embedded and persisted
|
|
9
|
+
# - :skipped: rows skipped because they were unchanged or incomplete
|
|
10
|
+
# - :failed: count of rows that failed during rebuild
|
|
11
|
+
# - :failed_ids: record IDs that failed during rebuild
|
|
12
|
+
# - :failed_errors: error class/message keyed by failed record ID
|
|
13
|
+
class RebuildEmbeddings
|
|
14
|
+
def initialize(store:, embedder:)
|
|
15
|
+
@store = store
|
|
16
|
+
@embedder = embedder
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call(scope:, stale_only: true, batch_size: 100)
|
|
20
|
+
batch_size = Integer(batch_size)
|
|
21
|
+
raise ArgumentError, "batch_size must be greater than 0" unless batch_size.positive?
|
|
22
|
+
|
|
23
|
+
counts = {
|
|
24
|
+
processed: 0,
|
|
25
|
+
updated: 0,
|
|
26
|
+
skipped: 0,
|
|
27
|
+
failed: 0,
|
|
28
|
+
failed_ids: [],
|
|
29
|
+
failed_errors: {}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
after_id = nil
|
|
33
|
+
legacy_index = 0
|
|
34
|
+
legacy_records = nil
|
|
35
|
+
processed_ids = Set.new
|
|
36
|
+
processed_nil_records = Set.new
|
|
37
|
+
loop do
|
|
38
|
+
batch_result = fetch_batch(scope:, batch_size:, after_id:, legacy_records:, legacy_index:)
|
|
39
|
+
legacy_records = batch_result[:legacy_records] if batch_result[:legacy_records]
|
|
40
|
+
batch = batch_result[:records]
|
|
41
|
+
if batch.empty?
|
|
42
|
+
break if legacy_records
|
|
43
|
+
|
|
44
|
+
# Reconcile with one stable snapshot: accepting keyset keywords does
|
|
45
|
+
# not guarantee that an adapter returns complete, ordered pages.
|
|
46
|
+
legacy_records = Array(@store.all(scope: scope))
|
|
47
|
+
legacy_index = 0
|
|
48
|
+
next
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
cursor_record = batch.reverse.find { |candidate| !candidate.id.nil? } unless legacy_records
|
|
52
|
+
|
|
53
|
+
if !legacy_records && fallback_before_processing?(batch:, after_id:)
|
|
54
|
+
legacy_records = Array(@store.all(scope: scope))
|
|
55
|
+
legacy_index = 0
|
|
56
|
+
batch = legacy_records.slice(legacy_index, batch_size) || []
|
|
57
|
+
break if batch.empty?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
batch.each do |record|
|
|
61
|
+
identity = record.id.nil? ? record.object_id : record.id
|
|
62
|
+
seen = record.id.nil? ? processed_nil_records : processed_ids
|
|
63
|
+
next unless seen.add?(identity)
|
|
64
|
+
|
|
65
|
+
counts[:processed] += 1
|
|
66
|
+
|
|
67
|
+
if record.id.nil?
|
|
68
|
+
counts[:skipped] += 1
|
|
69
|
+
next
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
if stale_only && !stale?(record)
|
|
73
|
+
counts[:skipped] += 1
|
|
74
|
+
next
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
begin
|
|
78
|
+
rebuilt = record.with(embedding: @embedder.embed(record.content))
|
|
79
|
+
rebuilt = EmbeddingMetadata.attach(rebuilt, embedder: @embedder)
|
|
80
|
+
updated = @store.update(scope: scope, id: record.id, record: rebuilt)
|
|
81
|
+
raise Engram::Error, "memory update was not applied" unless updated && updated != 0
|
|
82
|
+
|
|
83
|
+
counts[:updated] += 1
|
|
84
|
+
rescue => error
|
|
85
|
+
counts[:failed] += 1
|
|
86
|
+
record_id = record.id
|
|
87
|
+
counts[:failed_ids] << record_id
|
|
88
|
+
counts[:failed_errors][record_id] = {
|
|
89
|
+
class: error.class.name,
|
|
90
|
+
message: error.message
|
|
91
|
+
}
|
|
92
|
+
warn "Rebuild failed for record ##{record_id}: #{error.class}: #{error.message}"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
if legacy_records
|
|
97
|
+
legacy_index += batch.length
|
|
98
|
+
next
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
if cursor_record.nil?
|
|
102
|
+
legacy_records = Array(@store.all(scope: scope))
|
|
103
|
+
legacy_index = 0
|
|
104
|
+
next
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
if fallback_to_legacy_batching?(batch:, cursor_record:)
|
|
108
|
+
legacy_records = Array(@store.all(scope: scope))
|
|
109
|
+
legacy_index = 0
|
|
110
|
+
next
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
after_id = cursor_record.id
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
{scope: scope, **counts}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
def fetch_batch(scope:, batch_size:, after_id:, legacy_records:, legacy_index:)
|
|
122
|
+
if legacy_records
|
|
123
|
+
return {records: legacy_records.slice(legacy_index, batch_size) || [], legacy_records:}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
records = modern_batch(scope:, batch_size:, after_id:)
|
|
127
|
+
return {records:} if records
|
|
128
|
+
|
|
129
|
+
legacy_records = Array(@store.all(scope: scope))
|
|
130
|
+
{records: legacy_records.slice(legacy_index, batch_size) || [], legacy_records:}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def modern_batch(scope:, batch_size:, after_id:)
|
|
134
|
+
return unless batched_all_supported?
|
|
135
|
+
|
|
136
|
+
@store.all(scope: scope, limit: batch_size, after_id: after_id)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def batched_all_supported?
|
|
140
|
+
parameters = @store.method(:all).parameters
|
|
141
|
+
keyword_names = parameters.filter_map do |kind, name|
|
|
142
|
+
name if [:keyreq, :key].include?(kind)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
keyword_names.include?(:limit) && keyword_names.include?(:after_id)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def fallback_to_legacy_batching?(batch:, cursor_record:)
|
|
149
|
+
cursor_record != batch.last
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def fallback_before_processing?(batch:, after_id:)
|
|
153
|
+
ids = batch.filter_map(&:id)
|
|
154
|
+
return true if ids.uniq.length != ids.length
|
|
155
|
+
return false if after_id.nil?
|
|
156
|
+
return true if batch.any? { |record| record.id.nil? }
|
|
157
|
+
|
|
158
|
+
batch.any? { |record| record.id && record.id <= after_id }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def stale?(record)
|
|
162
|
+
stored = EmbeddingMetadata.extract(record.metadata)
|
|
163
|
+
return true if stored.empty?
|
|
164
|
+
|
|
165
|
+
expected = expected_metadata(record)
|
|
166
|
+
return true unless expected
|
|
167
|
+
|
|
168
|
+
required = %w[adapter provider model dimensions fingerprint]
|
|
169
|
+
return true if required.any? { |key| stored[key] != expected[key] }
|
|
170
|
+
|
|
171
|
+
return true if stored["dimensions"] &&
|
|
172
|
+
record.embedding.respond_to?(:length) &&
|
|
173
|
+
record.embedding.length != stored["dimensions"].to_i
|
|
174
|
+
|
|
175
|
+
false
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# What the active embedder would produce for a rebuilt row. Declared dimensions take
|
|
179
|
+
# precedence so dimension-only configuration drift marks rows stale; fall back to the
|
|
180
|
+
# record's current vector length for embedders that do not declare dimensions.
|
|
181
|
+
def expected_metadata(record)
|
|
182
|
+
declared = EmbeddingMetadata.for_embedder(@embedder)
|
|
183
|
+
return declared if declared.nil? || declared["dimensions"]
|
|
184
|
+
|
|
185
|
+
EmbeddingMetadata.for_embedder(@embedder, embedding: record.embedding)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -36,11 +36,19 @@ module Engram
|
|
|
36
36
|
)
|
|
37
37
|
Engram::Instrumentation.instrument("recall", payload) do
|
|
38
38
|
embedding = @embedder.embed(query)
|
|
39
|
+
embedding_metadata = Engram::EmbeddingMetadata.for_embedder(@embedder, embedding: embedding)
|
|
39
40
|
pool_limit = reranking? ? limit * @pool_factor : limit
|
|
40
|
-
pool =
|
|
41
|
+
pool = Engram::EmbeddingMetadata.search(
|
|
42
|
+
@store,
|
|
43
|
+
embedding: embedding,
|
|
44
|
+
embedding_metadata: embedding_metadata,
|
|
45
|
+
scope: scope,
|
|
46
|
+
limit: pool_limit,
|
|
47
|
+
kinds: kinds
|
|
48
|
+
)
|
|
41
49
|
|
|
42
50
|
results = (reranking? ? rerank(pool, embedding) : pool).first(limit)
|
|
43
|
-
touch(results) if @touch
|
|
51
|
+
touch(results, scope) if @touch
|
|
44
52
|
payload[:result_count] = results.size
|
|
45
53
|
payload[:candidate_count] = pool.size
|
|
46
54
|
results
|
|
@@ -72,8 +80,8 @@ module Engram
|
|
|
72
80
|
0.5**(age / @recency_halflife)
|
|
73
81
|
end
|
|
74
82
|
|
|
75
|
-
def touch(records)
|
|
76
|
-
records.each { |record| @store.touch(id: record.id, at: Time.now) if record.id }
|
|
83
|
+
def touch(records, scope)
|
|
84
|
+
records.each { |record| @store.touch(scope: scope, id: record.id, at: Time.now) if record.id }
|
|
77
85
|
end
|
|
78
86
|
end
|
|
79
87
|
end
|
data/lib/engram/version.rb
CHANGED
data/lib/engram.rb
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
require_relative "engram/version"
|
|
4
4
|
require_relative "engram/configuration"
|
|
5
5
|
require_relative "engram/math"
|
|
6
|
+
require_relative "engram/reserved_metadata"
|
|
7
|
+
require_relative "engram/embedding_metadata"
|
|
8
|
+
require_relative "engram/provenance"
|
|
6
9
|
require_relative "engram/memory_kind"
|
|
7
10
|
require_relative "engram/record"
|
|
8
11
|
require_relative "engram/decision"
|
|
@@ -24,6 +27,7 @@ require_relative "engram/use_cases/recall"
|
|
|
24
27
|
require_relative "engram/use_cases/inject"
|
|
25
28
|
require_relative "engram/use_cases/observe"
|
|
26
29
|
require_relative "engram/use_cases/forget"
|
|
30
|
+
require_relative "engram/use_cases/rebuild_embeddings"
|
|
27
31
|
|
|
28
32
|
# Built-in adapters (pure Ruby, no external deps)
|
|
29
33
|
require_relative "engram/adapters/in_memory_store"
|
|
@@ -51,6 +55,9 @@ require_relative "engram/integrations/ruby_llm"
|
|
|
51
55
|
module Engram
|
|
52
56
|
class Error < StandardError; end
|
|
53
57
|
|
|
58
|
+
# Raised when a live claim suppresses an observation that has not completed.
|
|
59
|
+
class ObservationInProgressError < Error; end
|
|
60
|
+
|
|
54
61
|
class << self
|
|
55
62
|
def config
|
|
56
63
|
@config ||= Configuration.new
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: engram
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alexandr Kholodniak
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: |
|
|
14
14
|
Engram gives AI agents durable, long-term memory. It recalls relevant facts about a
|
|
@@ -36,6 +36,7 @@ files:
|
|
|
36
36
|
- lib/engram/consolidators/heuristic_consolidator.rb
|
|
37
37
|
- lib/engram/consolidators/llm_consolidator.rb
|
|
38
38
|
- lib/engram/decision.rb
|
|
39
|
+
- lib/engram/embedding_metadata.rb
|
|
39
40
|
- lib/engram/extractors/llm_extractor.rb
|
|
40
41
|
- lib/engram/instrumentation.rb
|
|
41
42
|
- lib/engram/integrations/ruby_llm.rb
|
|
@@ -50,15 +51,19 @@ files:
|
|
|
50
51
|
- lib/engram/ports/extractor.rb
|
|
51
52
|
- lib/engram/ports/memory_store.rb
|
|
52
53
|
- lib/engram/ports/processed_turns.rb
|
|
54
|
+
- lib/engram/provenance.rb
|
|
53
55
|
- lib/engram/rails/cache_processed_turns.rb
|
|
54
56
|
- lib/engram/rails/has_memory.rb
|
|
55
57
|
- lib/engram/rails/observe_job.rb
|
|
58
|
+
- lib/engram/rails/tasks.rake
|
|
56
59
|
- lib/engram/railtie.rb
|
|
57
60
|
- lib/engram/record.rb
|
|
61
|
+
- lib/engram/reserved_metadata.rb
|
|
58
62
|
- lib/engram/turn_digest.rb
|
|
59
63
|
- lib/engram/use_cases/forget.rb
|
|
60
64
|
- lib/engram/use_cases/inject.rb
|
|
61
65
|
- lib/engram/use_cases/observe.rb
|
|
66
|
+
- lib/engram/use_cases/rebuild_embeddings.rb
|
|
62
67
|
- lib/engram/use_cases/recall.rb
|
|
63
68
|
- lib/engram/version.rb
|
|
64
69
|
- lib/generators/engram/install_generator.rb
|
|
@@ -89,7 +94,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
89
94
|
- !ruby/object:Gem::Version
|
|
90
95
|
version: '0'
|
|
91
96
|
requirements: []
|
|
92
|
-
rubygems_version: 3.5.
|
|
97
|
+
rubygems_version: 3.5.22
|
|
93
98
|
signing_key:
|
|
94
99
|
specification_version: 4
|
|
95
100
|
summary: Long-term memory for AI agents in Ruby — stored in your own Postgres.
|