engram 0.3.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/CHANGELOG.md +38 -0
- data/LICENSE.txt +21 -0
- data/README.md +202 -0
- data/lib/engram/adapters/fake_completion.rb +32 -0
- data/lib/engram/adapters/in_memory_processed_turns.rb +29 -0
- data/lib/engram/adapters/in_memory_store.rb +58 -0
- data/lib/engram/adapters/null_embedder.rb +28 -0
- data/lib/engram/adapters/pgvector_store.rb +90 -0
- data/lib/engram/adapters/ruby_llm_completion.rb +43 -0
- data/lib/engram/adapters/ruby_llm_embedder.rb +35 -0
- data/lib/engram/configuration.rb +28 -0
- data/lib/engram/consolidators/heuristic_consolidator.rb +31 -0
- data/lib/engram/consolidators/llm_consolidator.rb +98 -0
- data/lib/engram/decision.rb +27 -0
- data/lib/engram/extractors/llm_extractor.rb +85 -0
- data/lib/engram/integrations/ruby_llm.rb +40 -0
- data/lib/engram/math.rb +23 -0
- data/lib/engram/memory.rb +105 -0
- data/lib/engram/ports/completion.rb +15 -0
- data/lib/engram/ports/consolidator.rb +17 -0
- data/lib/engram/ports/embedder.rb +19 -0
- data/lib/engram/ports/extractor.rb +15 -0
- data/lib/engram/ports/memory_store.rb +41 -0
- data/lib/engram/ports/processed_turns.rb +20 -0
- data/lib/engram/rails/cache_processed_turns.rb +31 -0
- data/lib/engram/rails/has_memory.rb +32 -0
- data/lib/engram/rails/observe_job.rb +11 -0
- data/lib/engram/railtie.rb +23 -0
- data/lib/engram/record.rb +35 -0
- data/lib/engram/turn_digest.rb +28 -0
- data/lib/engram/use_cases/forget.rb +28 -0
- data/lib/engram/use_cases/inject.rb +22 -0
- data/lib/engram/use_cases/observe.rb +59 -0
- data/lib/engram/use_cases/recall.rb +69 -0
- data/lib/engram/version.rb +5 -0
- data/lib/engram.rb +66 -0
- data/lib/generators/engram/install_generator.rb +48 -0
- data/lib/generators/engram/templates/create_engram_memories.rb.tt +24 -0
- data/lib/generators/engram/templates/initializer.rb.tt +12 -0
- data/lib/generators/engram/templates/memory_record.rb.tt +9 -0
- metadata +91 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Engram
|
|
4
|
+
module UseCases
|
|
5
|
+
# Prune stale memories from a scope. A memory is stale when its last activity
|
|
6
|
+
# (last_accessed_at, or created_at if never accessed) is older than the cutoff.
|
|
7
|
+
# `min_importance` keeps important memories even when they are old: only memories with
|
|
8
|
+
# importance below it are eligible. The default forgets all stale memories.
|
|
9
|
+
class Forget
|
|
10
|
+
def initialize(store:)
|
|
11
|
+
@store = store
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Returns the Array<Record> that were forgotten.
|
|
15
|
+
def call(scope:, older_than:, min_importance: Float::INFINITY, now: Time.now)
|
|
16
|
+
cutoff = now - older_than
|
|
17
|
+
|
|
18
|
+
stale = @store.all(scope: scope).select do |record|
|
|
19
|
+
timestamp = record.last_accessed_at || record.created_at
|
|
20
|
+
timestamp && timestamp < cutoff && record.importance.to_f < min_importance
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
stale.each { |record| @store.delete(id: record.id) if record.id }
|
|
24
|
+
stale
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Engram
|
|
4
|
+
module UseCases
|
|
5
|
+
# Render recalled memories into a prompt as a clearly delimited block.
|
|
6
|
+
class Inject
|
|
7
|
+
DEFAULT_HEADER = "# What you remember about the user"
|
|
8
|
+
|
|
9
|
+
def initialize(header: DEFAULT_HEADER)
|
|
10
|
+
@header = header
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Returns a new prompt string. If there are no memories, the prompt is unchanged.
|
|
14
|
+
def call(prompt:, memories:)
|
|
15
|
+
return prompt if memories.nil? || memories.empty?
|
|
16
|
+
|
|
17
|
+
block = memories.map { |m| "- #{m.content}" }.join("\n")
|
|
18
|
+
"#{prompt}\n\n#{@header}:\n#{block}"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Engram
|
|
4
|
+
module UseCases
|
|
5
|
+
# Orchestrates a single observed turn: extract candidate facts, consolidate them
|
|
6
|
+
# against existing memory, and apply the resulting decisions to the store.
|
|
7
|
+
# Pure and synchronous — async execution is a Rails concern (see ObserveJob).
|
|
8
|
+
#
|
|
9
|
+
# When a ProcessedTurns store and an idempotency_key are provided, a turn that was
|
|
10
|
+
# already processed is skipped (no extraction, no duplicate memories).
|
|
11
|
+
class Observe
|
|
12
|
+
def initialize(store:, extractor:, consolidator:, processed_turns: nil)
|
|
13
|
+
@store = store
|
|
14
|
+
@extractor = extractor
|
|
15
|
+
@consolidator = consolidator
|
|
16
|
+
@processed_turns = processed_turns
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Returns the Array<Decision> that were applied (empty if skipped or nothing found).
|
|
20
|
+
def call(messages:, scope:, idempotency_key: nil)
|
|
21
|
+
return [] if already_processed?(idempotency_key)
|
|
22
|
+
|
|
23
|
+
candidates = @extractor.extract(messages: messages, scope: scope)
|
|
24
|
+
if candidates.empty?
|
|
25
|
+
mark_processed(idempotency_key)
|
|
26
|
+
return []
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
decisions = @consolidator.reconcile_all(candidates: candidates, scope: scope)
|
|
30
|
+
decisions.each { |decision| apply(decision) }
|
|
31
|
+
mark_processed(idempotency_key)
|
|
32
|
+
decisions
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def already_processed?(key)
|
|
38
|
+
!!(key && @processed_turns&.seen?(key))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def mark_processed(key)
|
|
42
|
+
@processed_turns.record(key) if key && @processed_turns
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def apply(decision)
|
|
46
|
+
case decision.action
|
|
47
|
+
when :add
|
|
48
|
+
@store.add(decision.candidate)
|
|
49
|
+
when :update
|
|
50
|
+
@store.update(id: decision.target_id, record: decision.candidate) if decision.target_id
|
|
51
|
+
when :forget
|
|
52
|
+
@store.delete(id: decision.target_id) if decision.target_id
|
|
53
|
+
when :noop
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Engram
|
|
4
|
+
module UseCases
|
|
5
|
+
# Embed a query and fetch the most relevant memories for a scope.
|
|
6
|
+
#
|
|
7
|
+
# By default this is pure vector similarity (the store's own ordering). When
|
|
8
|
+
# importance_weight or recency_weight are non-zero, it fetches a larger candidate pool
|
|
9
|
+
# and re-ranks by a composite score: similarity + importance + recency. With both
|
|
10
|
+
# weights at zero (the default) behaviour is identical to plain similarity search.
|
|
11
|
+
class Recall
|
|
12
|
+
DEFAULT_HALFLIFE = 30 * 24 * 60 * 60 # 30 days, in seconds
|
|
13
|
+
DEFAULT_POOL_FACTOR = 4
|
|
14
|
+
|
|
15
|
+
def initialize(store:, embedder:, importance_weight: 0.0, recency_weight: 0.0,
|
|
16
|
+
recency_halflife: DEFAULT_HALFLIFE, pool_factor: DEFAULT_POOL_FACTOR, touch: false)
|
|
17
|
+
@store = store
|
|
18
|
+
@embedder = embedder
|
|
19
|
+
@importance_weight = importance_weight.to_f
|
|
20
|
+
@recency_weight = recency_weight.to_f
|
|
21
|
+
@recency_halflife = recency_halflife.to_f
|
|
22
|
+
@pool_factor = pool_factor
|
|
23
|
+
@touch = touch
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns Array<Record>, most relevant first.
|
|
27
|
+
def call(query, scope:, limit: Engram.config.default_limit)
|
|
28
|
+
raise ArgumentError, "query must be a non-empty string" if query.to_s.strip.empty?
|
|
29
|
+
|
|
30
|
+
embedding = @embedder.embed(query)
|
|
31
|
+
pool_limit = reranking? ? limit * @pool_factor : limit
|
|
32
|
+
pool = @store.search(embedding: embedding, scope: scope, limit: pool_limit)
|
|
33
|
+
|
|
34
|
+
results = (reranking? ? rerank(pool, embedding) : pool).first(limit)
|
|
35
|
+
touch(results) if @touch
|
|
36
|
+
results
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def reranking?
|
|
42
|
+
!@importance_weight.zero? || !@recency_weight.zero?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def rerank(records, query_embedding)
|
|
46
|
+
now = Time.now
|
|
47
|
+
records.sort_by do |record|
|
|
48
|
+
similarity = Engram::Math.cosine_similarity(query_embedding, record.embedding)
|
|
49
|
+
score = similarity +
|
|
50
|
+
(@importance_weight * record.importance.to_f) +
|
|
51
|
+
(@recency_weight * recency(record, now))
|
|
52
|
+
-score
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def recency(record, now)
|
|
57
|
+
timestamp = record.last_accessed_at || record.created_at
|
|
58
|
+
return 0.0 unless timestamp
|
|
59
|
+
|
|
60
|
+
age = now - timestamp
|
|
61
|
+
0.5**(age / @recency_halflife)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def touch(records)
|
|
65
|
+
records.each { |record| @store.touch(id: record.id, at: Time.now) if record.id }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
data/lib/engram.rb
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "engram/version"
|
|
4
|
+
require_relative "engram/configuration"
|
|
5
|
+
require_relative "engram/math"
|
|
6
|
+
require_relative "engram/record"
|
|
7
|
+
require_relative "engram/decision"
|
|
8
|
+
require_relative "engram/turn_digest"
|
|
9
|
+
|
|
10
|
+
# Ports (contracts)
|
|
11
|
+
require_relative "engram/ports/memory_store"
|
|
12
|
+
require_relative "engram/ports/embedder"
|
|
13
|
+
require_relative "engram/ports/completion"
|
|
14
|
+
require_relative "engram/ports/extractor"
|
|
15
|
+
require_relative "engram/ports/consolidator"
|
|
16
|
+
require_relative "engram/ports/processed_turns"
|
|
17
|
+
|
|
18
|
+
# Use cases
|
|
19
|
+
require_relative "engram/use_cases/recall"
|
|
20
|
+
require_relative "engram/use_cases/inject"
|
|
21
|
+
require_relative "engram/use_cases/observe"
|
|
22
|
+
require_relative "engram/use_cases/forget"
|
|
23
|
+
|
|
24
|
+
# Built-in adapters (pure Ruby, no external deps)
|
|
25
|
+
require_relative "engram/adapters/in_memory_store"
|
|
26
|
+
require_relative "engram/adapters/null_embedder"
|
|
27
|
+
require_relative "engram/adapters/fake_completion"
|
|
28
|
+
require_relative "engram/adapters/in_memory_processed_turns"
|
|
29
|
+
|
|
30
|
+
# Optional adapters. These reference external libraries (neighbor, ruby_llm) only at
|
|
31
|
+
# call time, so requiring the files here is safe even if those gems are absent.
|
|
32
|
+
require_relative "engram/adapters/pgvector_store"
|
|
33
|
+
require_relative "engram/adapters/ruby_llm_embedder"
|
|
34
|
+
require_relative "engram/adapters/ruby_llm_completion"
|
|
35
|
+
|
|
36
|
+
# Pipeline stages (v0.2)
|
|
37
|
+
require_relative "engram/extractors/llm_extractor"
|
|
38
|
+
require_relative "engram/consolidators/heuristic_consolidator"
|
|
39
|
+
require_relative "engram/consolidators/llm_consolidator"
|
|
40
|
+
|
|
41
|
+
require_relative "engram/memory"
|
|
42
|
+
|
|
43
|
+
# Optional integrations (pure Ruby; reference external libs only at call time)
|
|
44
|
+
require_relative "engram/integrations/ruby_llm"
|
|
45
|
+
|
|
46
|
+
# Public entrypoint and configuration store.
|
|
47
|
+
module Engram
|
|
48
|
+
class Error < StandardError; end
|
|
49
|
+
|
|
50
|
+
class << self
|
|
51
|
+
def config
|
|
52
|
+
@config ||= Configuration.new
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def configure
|
|
56
|
+
yield config
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Reset configuration (primarily for tests).
|
|
60
|
+
def reset!
|
|
61
|
+
@config = Configuration.new
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
require_relative "engram/railtie" if defined?(Rails::Railtie)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module Engram
|
|
7
|
+
module Generators
|
|
8
|
+
# `bin/rails generate engram:install [--dimensions=1536]`
|
|
9
|
+
# Creates the migration, an initializer, and the AR model.
|
|
10
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
11
|
+
include ::Rails::Generators::Migration
|
|
12
|
+
|
|
13
|
+
source_root File.expand_path("templates", __dir__)
|
|
14
|
+
|
|
15
|
+
DEFAULT_DIMENSIONS = 1536
|
|
16
|
+
|
|
17
|
+
class_option :dimensions, type: :numeric, default: DEFAULT_DIMENSIONS,
|
|
18
|
+
desc: "Embedding dimensions (match your embedding model)"
|
|
19
|
+
|
|
20
|
+
def create_migration_file
|
|
21
|
+
migration_template "create_engram_memories.rb.tt",
|
|
22
|
+
"db/migrate/create_engram_memories.rb"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def create_initializer
|
|
26
|
+
template "initializer.rb.tt", "config/initializers/engram.rb"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def create_model
|
|
30
|
+
template "memory_record.rb.tt", "app/models/engram/memory_record.rb"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.next_migration_number(dir)
|
|
34
|
+
::ActiveRecord::Generators::Base.next_migration_number(dir)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def dimensions
|
|
40
|
+
options[:dimensions]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def migration_version
|
|
44
|
+
"#{::ActiveRecord::VERSION::MAJOR}.#{::ActiveRecord::VERSION::MINOR}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateEngramMemories < ActiveRecord::Migration[<%= migration_version %>]
|
|
4
|
+
def change
|
|
5
|
+
enable_extension "vector" unless extension_enabled?("vector")
|
|
6
|
+
|
|
7
|
+
create_table :engram_memories do |t|
|
|
8
|
+
t.string :scope, null: false
|
|
9
|
+
t.text :content, null: false
|
|
10
|
+
t.string :kind, null: false, default: "semantic"
|
|
11
|
+
t.float :importance, null: false, default: 1.0
|
|
12
|
+
t.jsonb :metadata, null: false, default: {}
|
|
13
|
+
t.vector :embedding, limit: <%= dimensions %>
|
|
14
|
+
t.datetime :last_accessed_at
|
|
15
|
+
|
|
16
|
+
t.timestamps
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
add_index :engram_memories, :scope
|
|
20
|
+
|
|
21
|
+
# For larger datasets, add an approximate index (requires data present first):
|
|
22
|
+
# add_index :engram_memories, :embedding, using: :hnsw, opclass: :vector_cosine_ops
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Engram.configure do |config|
|
|
4
|
+
# Persist memories in your own Postgres via pgvector + neighbor.
|
|
5
|
+
config.store = Engram::Adapters::PgvectorStore.new
|
|
6
|
+
|
|
7
|
+
# Generate embeddings via RubyLLM. Configure RubyLLM separately with your provider keys.
|
|
8
|
+
config.embedder = Engram::Adapters::RubyLLMEmbedder.new
|
|
9
|
+
|
|
10
|
+
# How many memories to recall by default.
|
|
11
|
+
config.default_limit = 5
|
|
12
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: engram
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.3.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Alexandr Kholodniak
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-05-25 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: |
|
|
14
|
+
Engram gives AI agents durable, long-term memory. It recalls relevant facts about a
|
|
15
|
+
user and injects them into the prompt, so an agent appears to remember across sessions.
|
|
16
|
+
Framework-agnostic core with a ports-and-adapters design; first-class Rails and RubyLLM
|
|
17
|
+
integration. Your memories live in your own database — no external memory service.
|
|
18
|
+
email:
|
|
19
|
+
- alexandrkholodniak@gmail.com
|
|
20
|
+
executables: []
|
|
21
|
+
extensions: []
|
|
22
|
+
extra_rdoc_files: []
|
|
23
|
+
files:
|
|
24
|
+
- CHANGELOG.md
|
|
25
|
+
- LICENSE.txt
|
|
26
|
+
- README.md
|
|
27
|
+
- lib/engram.rb
|
|
28
|
+
- lib/engram/adapters/fake_completion.rb
|
|
29
|
+
- lib/engram/adapters/in_memory_processed_turns.rb
|
|
30
|
+
- lib/engram/adapters/in_memory_store.rb
|
|
31
|
+
- lib/engram/adapters/null_embedder.rb
|
|
32
|
+
- lib/engram/adapters/pgvector_store.rb
|
|
33
|
+
- lib/engram/adapters/ruby_llm_completion.rb
|
|
34
|
+
- lib/engram/adapters/ruby_llm_embedder.rb
|
|
35
|
+
- lib/engram/configuration.rb
|
|
36
|
+
- lib/engram/consolidators/heuristic_consolidator.rb
|
|
37
|
+
- lib/engram/consolidators/llm_consolidator.rb
|
|
38
|
+
- lib/engram/decision.rb
|
|
39
|
+
- lib/engram/extractors/llm_extractor.rb
|
|
40
|
+
- lib/engram/integrations/ruby_llm.rb
|
|
41
|
+
- lib/engram/math.rb
|
|
42
|
+
- lib/engram/memory.rb
|
|
43
|
+
- lib/engram/ports/completion.rb
|
|
44
|
+
- lib/engram/ports/consolidator.rb
|
|
45
|
+
- lib/engram/ports/embedder.rb
|
|
46
|
+
- lib/engram/ports/extractor.rb
|
|
47
|
+
- lib/engram/ports/memory_store.rb
|
|
48
|
+
- lib/engram/ports/processed_turns.rb
|
|
49
|
+
- lib/engram/rails/cache_processed_turns.rb
|
|
50
|
+
- lib/engram/rails/has_memory.rb
|
|
51
|
+
- lib/engram/rails/observe_job.rb
|
|
52
|
+
- lib/engram/railtie.rb
|
|
53
|
+
- lib/engram/record.rb
|
|
54
|
+
- lib/engram/turn_digest.rb
|
|
55
|
+
- lib/engram/use_cases/forget.rb
|
|
56
|
+
- lib/engram/use_cases/inject.rb
|
|
57
|
+
- lib/engram/use_cases/observe.rb
|
|
58
|
+
- lib/engram/use_cases/recall.rb
|
|
59
|
+
- lib/engram/version.rb
|
|
60
|
+
- lib/generators/engram/install_generator.rb
|
|
61
|
+
- lib/generators/engram/templates/create_engram_memories.rb.tt
|
|
62
|
+
- lib/generators/engram/templates/initializer.rb.tt
|
|
63
|
+
- lib/generators/engram/templates/memory_record.rb.tt
|
|
64
|
+
homepage: https://github.com/kholdrex/engram
|
|
65
|
+
licenses:
|
|
66
|
+
- MIT
|
|
67
|
+
metadata:
|
|
68
|
+
homepage_uri: https://github.com/kholdrex/engram
|
|
69
|
+
source_code_uri: https://github.com/kholdrex/engram
|
|
70
|
+
changelog_uri: https://github.com/kholdrex/engram/blob/main/CHANGELOG.md
|
|
71
|
+
rubygems_mfa_required: 'true'
|
|
72
|
+
post_install_message:
|
|
73
|
+
rdoc_options: []
|
|
74
|
+
require_paths:
|
|
75
|
+
- lib
|
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: 3.2.0
|
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
82
|
+
requirements:
|
|
83
|
+
- - ">="
|
|
84
|
+
- !ruby/object:Gem::Version
|
|
85
|
+
version: '0'
|
|
86
|
+
requirements: []
|
|
87
|
+
rubygems_version: 3.5.9
|
|
88
|
+
signing_key:
|
|
89
|
+
specification_version: 4
|
|
90
|
+
summary: Long-term memory for AI agents in Ruby — stored in your own Postgres.
|
|
91
|
+
test_files: []
|