llmemory 0.2.11 → 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 +4 -4
- data/README.md +23 -0
- data/lib/generators/llmemory/install/templates/create_llmemory_tables.rb +71 -0
- data/lib/generators/llmemory/upgrade_zero_mem/templates/create_llmemory_zero_mem_tables.rb +77 -0
- data/lib/generators/llmemory/upgrade_zero_mem/upgrade_zero_mem_generator.rb +19 -0
- data/lib/llmemory/cli/commands/zero_mem.rb +107 -0
- data/lib/llmemory/cli.rb +3 -0
- data/lib/llmemory/configuration.rb +59 -1
- data/lib/llmemory/instrumentation.rb +10 -1
- data/lib/llmemory/maintenance/cognitive_pass.rb +29 -1
- data/lib/llmemory/maintenance/zero_mem_ttl.rb +26 -0
- data/lib/llmemory/maintenance.rb +1 -0
- data/lib/llmemory/mcp/server.rb +2 -0
- data/lib/llmemory/mcp/store_helpers.rb +16 -0
- data/lib/llmemory/mcp/tools/memory_add_message.rb +17 -4
- data/lib/llmemory/mcp/tools/memory_consolidate.rb +9 -4
- data/lib/llmemory/mcp/tools/memory_maintain.rb +6 -1
- data/lib/llmemory/mcp/tools/memory_retrieve.rb +3 -1
- data/lib/llmemory/mcp/tools/memory_retrieve_evidence.rb +70 -0
- data/lib/llmemory/mcp/tools/memory_stats.rb +22 -0
- data/lib/llmemory/memory.rb +257 -9
- data/lib/llmemory/retrieval/bm25_scorer.rb +5 -0
- data/lib/llmemory/tokenizer.rb +18 -8
- data/lib/llmemory/vector_store/memory_store.rb +23 -1
- data/lib/llmemory/vector_store/openai_embeddings.rb +47 -21
- data/lib/llmemory/version.rb +1 -1
- data/lib/llmemory/zero_mem/answer_calibrator.rb +63 -0
- data/lib/llmemory/zero_mem/backfill.rb +43 -0
- data/lib/llmemory/zero_mem/budget_assembler.rb +62 -0
- data/lib/llmemory/zero_mem/circuit_breaker.rb +37 -0
- data/lib/llmemory/zero_mem/embedding_provider.rb +73 -0
- data/lib/llmemory/zero_mem/engine.rb +247 -0
- data/lib/llmemory/zero_mem/entity.rb +31 -0
- data/lib/llmemory/zero_mem/entity_extractor.rb +23 -0
- data/lib/llmemory/zero_mem/entity_index.rb +65 -0
- data/lib/llmemory/zero_mem/entity_mention.rb +35 -0
- data/lib/llmemory/zero_mem/errors.rb +7 -0
- data/lib/llmemory/zero_mem/evidence.rb +43 -0
- data/lib/llmemory/zero_mem/evidence_calibrator.rb +158 -0
- data/lib/llmemory/zero_mem/evidence_closure.rb +98 -0
- data/lib/llmemory/zero_mem/evidence_fusion.rb +57 -0
- data/lib/llmemory/zero_mem/evidence_set.rb +54 -0
- data/lib/llmemory/zero_mem/extractors/heuristic.rb +44 -0
- data/lib/llmemory/zero_mem/extractors/http.rb +58 -0
- data/lib/llmemory/zero_mem/graph_retriever.rb +107 -0
- data/lib/llmemory/zero_mem/hierarchy_builder.rb +123 -0
- data/lib/llmemory/zero_mem/hierarchy_retriever.rb +133 -0
- data/lib/llmemory/zero_mem/indexer.rb +73 -0
- data/lib/llmemory/zero_mem/mode.rb +30 -0
- data/lib/llmemory/zero_mem/pagerank.rb +42 -0
- data/lib/llmemory/zero_mem/query_profile.rb +48 -0
- data/lib/llmemory/zero_mem/query_profiler.rb +138 -0
- data/lib/llmemory/zero_mem/repair.rb +31 -0
- data/lib/llmemory/zero_mem/router.rb +69 -0
- data/lib/llmemory/zero_mem/sidecar_client.rb +82 -0
- data/lib/llmemory/zero_mem/storage.rb +124 -0
- data/lib/llmemory/zero_mem/storages/active_record.rb +311 -0
- data/lib/llmemory/zero_mem/storages/active_record_models.rb +21 -0
- data/lib/llmemory/zero_mem/storages/file.rb +67 -0
- data/lib/llmemory/zero_mem/storages/memory.rb +294 -0
- data/lib/llmemory/zero_mem/storages/postgres.rb +85 -0
- data/lib/llmemory/zero_mem/storages/snapshot_codec.rb +180 -0
- data/lib/llmemory/zero_mem/storages.rb +43 -0
- data/lib/llmemory/zero_mem/trace.rb +100 -0
- data/lib/llmemory/zero_mem/trace_state_link.rb +64 -0
- data/lib/llmemory/zero_mem/trace_unit.rb +83 -0
- data/lib/llmemory/zero_mem.rb +42 -0
- data/lib/llmemory.rb +7 -0
- data/lib/tasks/llmemory.rake +52 -0
- metadata +48 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 004e6d5f30c9eba38319382257934d0644635fd63555e854043f32d7ec4aa7b0
|
|
4
|
+
data.tar.gz: 9295714e79963b31a2c23eb6834a4c31b75c2d83a044ecce905726c5ad8a41b1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e56b9d1b8a537b1a134be39cc9e0f563eed3f7e20f0cef23bb8dc12aa7881c30512382e027ed5926f507faf09ee2ca691fc2c489fd16bdbf1e1562c7e5e74ca2
|
|
7
|
+
data.tar.gz: a34a15a7eab1202fe44ebcf85e828c81d99941b1102f88242b8efada40884328a22369dd7860f27449401bd8a123c80cdf02305c9d19b5a0215f1a30ebcd6f31
|
data/README.md
CHANGED
|
@@ -153,6 +153,29 @@ Llmemory.configure do |config|
|
|
|
153
153
|
end
|
|
154
154
|
```
|
|
155
155
|
|
|
156
|
+
### Zero-Mem (experimental, opt-in)
|
|
157
|
+
|
|
158
|
+
Zero-Mem keeps **immutable traces** as the source of truth and retrieves evidence with deterministic profiling, hierarchy, and entity-graph ranking — **without generative LLM calls** in memory operations (`memory_mode: :zero_mem`). Default remains `:classic`. This is experimental: it does not reproduce published benchmark numbers.
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
Llmemory.configure do |config|
|
|
162
|
+
config.memory_mode = :zero_mem # or :hybrid (traces + classic long-term; not zero-token)
|
|
163
|
+
config.zero_mem_trace_store = :memory # or :active_record in Rails apps
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
memory = Llmemory::Memory.new(user_id: "u1", session_id: "s1")
|
|
167
|
+
memory.add_message(role: :user, content: "Project Atlas uses Nimbus")
|
|
168
|
+
context = memory.retrieve("Which database does Atlas use?")
|
|
169
|
+
evidence = memory.retrieve_evidence("Which database does Atlas use?", explain: true)
|
|
170
|
+
|
|
171
|
+
# Explicit generative ops raise in :zero_mem
|
|
172
|
+
# memory.consolidate! # => Llmemory::GenerativeOperationDisabled
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
CLI: `llmemory zero-mem traces USER`, `status`, `reindex`, `search USER "query"`. MCP: `memory_retrieve` (text, same as before) and `memory_retrieve_evidence` (structured JSON + context).
|
|
176
|
+
|
|
177
|
+
Rollout: enable `zero_mem_shadow_write` while staying on `:classic` for reads; see [docs/zero_mem_limitations.md](docs/zero_mem_limitations.md). Benchmarks: [benchmarks/zero_mem/README.md](benchmarks/zero_mem/README.md).
|
|
178
|
+
|
|
156
179
|
## Consolidation policy
|
|
157
180
|
|
|
158
181
|
By default, llmemory consolidates every extracted fact into long-term memory. For agents with **authoritative live data** (tools, DB snapshots), configure a `Consolidation::Policy` to **drop** or **mark volatile** facts that should not be treated as stable memory (e.g. current job, plan status, task lists).
|
|
@@ -94,9 +94,80 @@ class CreateLlmemoryTables < ActiveRecord::Migration[7.0]
|
|
|
94
94
|
t.string :source_type, null: false
|
|
95
95
|
t.string :source_id, null: false
|
|
96
96
|
t.vector :embedding, limit: 1536
|
|
97
|
+
t.string :embedding_model
|
|
98
|
+
t.integer :embedding_dimensions
|
|
97
99
|
t.text :text_content
|
|
98
100
|
t.timestamps
|
|
99
101
|
end
|
|
100
102
|
add_index :llmemory_embeddings, [:user_id, :source_type, :source_id], unique: true
|
|
103
|
+
|
|
104
|
+
create_table :llmemory_traces, id: false do |t|
|
|
105
|
+
t.string :id, null: false, primary_key: true
|
|
106
|
+
t.string :user_id, null: false
|
|
107
|
+
t.string :session_id, null: false
|
|
108
|
+
t.string :boundary_id
|
|
109
|
+
t.bigint :sequence, null: false
|
|
110
|
+
t.string :role, null: false
|
|
111
|
+
t.text :content, null: false
|
|
112
|
+
t.text :search_tokens
|
|
113
|
+
t.string :content_sha256, null: false
|
|
114
|
+
t.datetime :occurred_at, null: false
|
|
115
|
+
t.datetime :ingested_at, null: false
|
|
116
|
+
t.jsonb :metadata, null: false, default: {}
|
|
117
|
+
t.string :idempotency_key
|
|
118
|
+
t.datetime :archived_at
|
|
119
|
+
t.timestamps
|
|
120
|
+
end
|
|
121
|
+
add_index :llmemory_traces, [:user_id, :session_id, :sequence], unique: true
|
|
122
|
+
add_index :llmemory_traces, [:user_id, :idempotency_key], unique: true, where: "idempotency_key IS NOT NULL"
|
|
123
|
+
add_index :llmemory_traces, [:user_id, :session_id, :occurred_at]
|
|
124
|
+
add_index :llmemory_traces, [:user_id, :boundary_id]
|
|
125
|
+
add_index :llmemory_traces, [:user_id, :archived_at]
|
|
126
|
+
|
|
127
|
+
create_table :llmemory_trace_state_links do |t|
|
|
128
|
+
t.string :user_id, null: false
|
|
129
|
+
t.string :state_key, null: false
|
|
130
|
+
t.string :trace_id, null: false
|
|
131
|
+
t.datetime :valid_from, null: false
|
|
132
|
+
t.datetime :valid_to
|
|
133
|
+
t.string :supersedes_trace_id
|
|
134
|
+
t.string :source, null: false, default: "explicit"
|
|
135
|
+
t.datetime :created_at, null: false
|
|
136
|
+
end
|
|
137
|
+
add_index :llmemory_trace_state_links, [:user_id, :state_key, :valid_from, :valid_to]
|
|
138
|
+
add_index :llmemory_trace_state_links, :trace_id
|
|
139
|
+
|
|
140
|
+
create_table :llmemory_trace_units, id: false do |t|
|
|
141
|
+
t.string :id, null: false, primary_key: true
|
|
142
|
+
t.string :user_id, null: false
|
|
143
|
+
t.string :trace_id, null: false
|
|
144
|
+
t.string :unit_type, null: false
|
|
145
|
+
t.jsonb :data, null: false, default: {}
|
|
146
|
+
t.datetime :archived_at
|
|
147
|
+
t.timestamps
|
|
148
|
+
end
|
|
149
|
+
add_index :llmemory_trace_units, [:user_id, :trace_id]
|
|
150
|
+
|
|
151
|
+
create_table :llmemory_trace_entities, id: false do |t|
|
|
152
|
+
t.string :id, null: false, primary_key: true
|
|
153
|
+
t.string :user_id, null: false
|
|
154
|
+
t.string :name, null: false
|
|
155
|
+
t.string :entity_type
|
|
156
|
+
t.datetime :archived_at
|
|
157
|
+
t.timestamps
|
|
158
|
+
end
|
|
159
|
+
add_index :llmemory_trace_entities, [:user_id, :name]
|
|
160
|
+
|
|
161
|
+
create_table :llmemory_entity_mentions, id: false do |t|
|
|
162
|
+
t.string :id, null: false, primary_key: true
|
|
163
|
+
t.string :user_id, null: false
|
|
164
|
+
t.string :trace_id, null: false
|
|
165
|
+
t.string :entity_id, null: false
|
|
166
|
+
t.integer :offset_start
|
|
167
|
+
t.integer :offset_end
|
|
168
|
+
t.datetime :archived_at
|
|
169
|
+
t.timestamps
|
|
170
|
+
end
|
|
171
|
+
add_index :llmemory_entity_mentions, [:user_id, :trace_id]
|
|
101
172
|
end
|
|
102
173
|
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateLlmemoryZeroMemTables < ActiveRecord::Migration[7.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :llmemory_traces, id: false do |t|
|
|
6
|
+
t.string :id, null: false, primary_key: true
|
|
7
|
+
t.string :user_id, null: false
|
|
8
|
+
t.string :session_id, null: false
|
|
9
|
+
t.string :boundary_id
|
|
10
|
+
t.bigint :sequence, null: false
|
|
11
|
+
t.string :role, null: false
|
|
12
|
+
t.text :content, null: false
|
|
13
|
+
t.text :search_tokens
|
|
14
|
+
t.string :content_sha256, null: false
|
|
15
|
+
t.datetime :occurred_at, null: false
|
|
16
|
+
t.datetime :ingested_at, null: false
|
|
17
|
+
t.jsonb :metadata, null: false, default: {}
|
|
18
|
+
t.string :idempotency_key
|
|
19
|
+
t.datetime :archived_at
|
|
20
|
+
t.timestamps
|
|
21
|
+
end
|
|
22
|
+
add_index :llmemory_traces, [:user_id, :session_id, :sequence], unique: true, name: "idx_llmemory_traces_user_session_sequence"
|
|
23
|
+
add_index :llmemory_traces, [:user_id, :idempotency_key], unique: true, where: "idempotency_key IS NOT NULL",
|
|
24
|
+
name: "idx_llmemory_traces_user_idempotency"
|
|
25
|
+
add_index :llmemory_traces, [:user_id, :session_id, :occurred_at], name: "idx_llmemory_traces_user_session_occurred"
|
|
26
|
+
add_index :llmemory_traces, [:user_id, :boundary_id], name: "idx_llmemory_traces_user_boundary"
|
|
27
|
+
add_index :llmemory_traces, [:user_id, :archived_at], name: "idx_llmemory_traces_user_archived"
|
|
28
|
+
|
|
29
|
+
create_table :llmemory_trace_state_links do |t|
|
|
30
|
+
t.string :user_id, null: false
|
|
31
|
+
t.string :state_key, null: false
|
|
32
|
+
t.string :trace_id, null: false
|
|
33
|
+
t.datetime :valid_from, null: false
|
|
34
|
+
t.datetime :valid_to
|
|
35
|
+
t.string :supersedes_trace_id
|
|
36
|
+
t.string :source, null: false, default: "explicit"
|
|
37
|
+
t.datetime :created_at, null: false
|
|
38
|
+
end
|
|
39
|
+
add_index :llmemory_trace_state_links, [:user_id, :state_key, :valid_from, :valid_to],
|
|
40
|
+
name: "idx_llmemory_trace_state_links_validity"
|
|
41
|
+
add_index :llmemory_trace_state_links, :trace_id, name: "idx_llmemory_trace_state_links_trace"
|
|
42
|
+
|
|
43
|
+
# Reserved for ZM2/ZM3 — created now to avoid chained migrations later.
|
|
44
|
+
create_table :llmemory_trace_units, id: false do |t|
|
|
45
|
+
t.string :id, null: false, primary_key: true
|
|
46
|
+
t.string :user_id, null: false
|
|
47
|
+
t.string :trace_id, null: false
|
|
48
|
+
t.string :unit_type, null: false
|
|
49
|
+
t.jsonb :data, null: false, default: {}
|
|
50
|
+
t.datetime :archived_at
|
|
51
|
+
t.timestamps
|
|
52
|
+
end
|
|
53
|
+
add_index :llmemory_trace_units, [:user_id, :trace_id], name: "idx_llmemory_trace_units_user_trace"
|
|
54
|
+
|
|
55
|
+
create_table :llmemory_trace_entities, id: false do |t|
|
|
56
|
+
t.string :id, null: false, primary_key: true
|
|
57
|
+
t.string :user_id, null: false
|
|
58
|
+
t.string :name, null: false
|
|
59
|
+
t.string :entity_type
|
|
60
|
+
t.datetime :archived_at
|
|
61
|
+
t.timestamps
|
|
62
|
+
end
|
|
63
|
+
add_index :llmemory_trace_entities, [:user_id, :name], name: "idx_llmemory_trace_entities_user_name"
|
|
64
|
+
|
|
65
|
+
create_table :llmemory_entity_mentions, id: false do |t|
|
|
66
|
+
t.string :id, null: false, primary_key: true
|
|
67
|
+
t.string :user_id, null: false
|
|
68
|
+
t.string :trace_id, null: false
|
|
69
|
+
t.string :entity_id, null: false
|
|
70
|
+
t.integer :offset_start
|
|
71
|
+
t.integer :offset_end
|
|
72
|
+
t.datetime :archived_at
|
|
73
|
+
t.timestamps
|
|
74
|
+
end
|
|
75
|
+
add_index :llmemory_entity_mentions, [:user_id, :trace_id], name: "idx_llmemory_entity_mentions_user_trace"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module Llmemory
|
|
6
|
+
module Generators
|
|
7
|
+
class UpgradeZeroMemGenerator < Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
desc "Add Zero-Mem trace tables (llmemory_traces, state links, reserved index tables)"
|
|
11
|
+
|
|
12
|
+
def copy_migration
|
|
13
|
+
timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
14
|
+
template "create_llmemory_zero_mem_tables.rb",
|
|
15
|
+
"db/migrate/#{timestamp}_create_llmemory_zero_mem_tables.rb"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "base"
|
|
5
|
+
|
|
6
|
+
module Llmemory
|
|
7
|
+
module Cli
|
|
8
|
+
module Commands
|
|
9
|
+
class ZeroMem < Base
|
|
10
|
+
def option_parser(parser)
|
|
11
|
+
parser.on("--session SESSION", "Session id (default: default)") { |v| @session_id = v }
|
|
12
|
+
parser.on("--explain", "Include explain payload on search") { @explain = true }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def execute(argv, _opts)
|
|
16
|
+
sub = argv.shift
|
|
17
|
+
case sub
|
|
18
|
+
when "traces"
|
|
19
|
+
run_traces(argv)
|
|
20
|
+
when "reindex"
|
|
21
|
+
run_reindex(argv)
|
|
22
|
+
when "status"
|
|
23
|
+
run_status(argv)
|
|
24
|
+
when "search"
|
|
25
|
+
run_search(argv)
|
|
26
|
+
else
|
|
27
|
+
print_usage
|
|
28
|
+
exit 1
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def print_usage
|
|
35
|
+
$stderr.puts <<~HELP
|
|
36
|
+
Usage:
|
|
37
|
+
llmemory zero-mem traces USER_ID [--session SESSION]
|
|
38
|
+
llmemory zero-mem reindex USER_ID [--session SESSION]
|
|
39
|
+
llmemory zero-mem status USER_ID [--session SESSION]
|
|
40
|
+
llmemory zero-mem search USER_ID "query" [--explain]
|
|
41
|
+
HELP
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def build_memory(user_id)
|
|
45
|
+
trace_store = Llmemory::ZeroMem::Storages.build
|
|
46
|
+
Llmemory::Memory.new(
|
|
47
|
+
user_id: user_id,
|
|
48
|
+
session_id: @session_id || "default",
|
|
49
|
+
trace_store: trace_store,
|
|
50
|
+
memory_mode: :zero_mem
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def run_traces(argv)
|
|
55
|
+
user_id = argv.shift
|
|
56
|
+
unless user_id
|
|
57
|
+
print_usage
|
|
58
|
+
exit 1
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
memory = build_memory(user_id)
|
|
62
|
+
traces = memory.trace_store.list_traces(user_id, session_id: @session_id || "default")
|
|
63
|
+
traces.each do |t|
|
|
64
|
+
puts "[#{t.id}] seq=#{t.sequence} #{t.role}: #{t.content[0, 120]}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def run_reindex(argv)
|
|
69
|
+
user_id = argv.shift
|
|
70
|
+
unless user_id
|
|
71
|
+
print_usage
|
|
72
|
+
exit 1
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
memory = build_memory(user_id)
|
|
76
|
+
status = memory.reindex_traces!(session_id: @session_id || "default")
|
|
77
|
+
puts JSON.generate(status)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def run_status(argv)
|
|
81
|
+
user_id = argv.shift
|
|
82
|
+
unless user_id
|
|
83
|
+
print_usage
|
|
84
|
+
exit 1
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
memory = build_memory(user_id)
|
|
88
|
+
puts JSON.generate(memory.zero_mem_status(session_id: @session_id || "default"))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def run_search(argv)
|
|
92
|
+
user_id = argv.shift
|
|
93
|
+
query = argv.join(" ").strip
|
|
94
|
+
unless user_id && !query.empty?
|
|
95
|
+
print_usage
|
|
96
|
+
exit 1
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
memory = build_memory(user_id)
|
|
100
|
+
result = memory.retrieve_evidence(query, explain: @explain == true)
|
|
101
|
+
puts result.to_context
|
|
102
|
+
puts JSON.generate(metrics: result.metrics, route: result.route) if @explain
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
data/lib/llmemory/cli.rb
CHANGED
|
@@ -13,6 +13,7 @@ require_relative "cli/commands/mine_skills"
|
|
|
13
13
|
require_relative "cli/commands/maintain"
|
|
14
14
|
require_relative "cli/commands/stats"
|
|
15
15
|
require_relative "cli/commands/search"
|
|
16
|
+
require_relative "cli/commands/zero_mem"
|
|
16
17
|
require_relative "cli/commands/mcp"
|
|
17
18
|
|
|
18
19
|
module Llmemory
|
|
@@ -60,6 +61,7 @@ module Llmemory
|
|
|
60
61
|
"mine_skills" => Cli::Commands::MineSkills,
|
|
61
62
|
"maintain" => Cli::Commands::Maintain,
|
|
62
63
|
"search" => Cli::Commands::Search,
|
|
64
|
+
"zero_mem" => Cli::Commands::ZeroMem,
|
|
63
65
|
"stats" => Cli::Commands::Stats,
|
|
64
66
|
"mcp" => Cli::Commands::Mcp
|
|
65
67
|
}[normalized]
|
|
@@ -87,6 +89,7 @@ module Llmemory
|
|
|
87
89
|
mine-skills USER_ID Mine reusable skills from episodes (--register to save)
|
|
88
90
|
maintain USER_ID Run the cognitive maintenance pass (reflect/mine/expire)
|
|
89
91
|
search USER_ID "query" Search in memory
|
|
92
|
+
zero-mem SUB ... Zero-Mem traces, reindex, status, search (experimental)
|
|
90
93
|
stats [USER_ID] Show statistics
|
|
91
94
|
mcp [serve] Start MCP server for LLM agents
|
|
92
95
|
|
|
@@ -59,7 +59,26 @@ module Llmemory
|
|
|
59
59
|
:dashboard_auth,
|
|
60
60
|
:dashboard_require_auth,
|
|
61
61
|
:consolidation_policy,
|
|
62
|
-
:retrieval_label_volatile
|
|
62
|
+
:retrieval_label_volatile,
|
|
63
|
+
:zero_mem_index_mode,
|
|
64
|
+
:zero_mem_trace_store,
|
|
65
|
+
:zero_mem_top_k,
|
|
66
|
+
:zero_mem_max_top_k,
|
|
67
|
+
:zero_mem_local_span,
|
|
68
|
+
:zero_mem_window_size,
|
|
69
|
+
:zero_mem_window_overlap,
|
|
70
|
+
:zero_mem_episode_gap_seconds,
|
|
71
|
+
:zero_mem_entity_extractor,
|
|
72
|
+
:zero_mem_embedding_provider,
|
|
73
|
+
:zero_mem_require_local_encoders,
|
|
74
|
+
:zero_mem_pagerank_damping,
|
|
75
|
+
:zero_mem_graph_weight,
|
|
76
|
+
:zero_mem_ner_http_url,
|
|
77
|
+
:memory_mode,
|
|
78
|
+
:zero_mem_ttl_days,
|
|
79
|
+
:zero_mem_sidecar_timeout_seconds,
|
|
80
|
+
:zero_mem_sidecar_failure_threshold,
|
|
81
|
+
:zero_mem_shadow_write
|
|
63
82
|
|
|
64
83
|
def initialize
|
|
65
84
|
@llm_provider = :openai
|
|
@@ -120,6 +139,44 @@ module Llmemory
|
|
|
120
139
|
@dashboard_require_auth = nil
|
|
121
140
|
@consolidation_policy = nil
|
|
122
141
|
@retrieval_label_volatile = false
|
|
142
|
+
@zero_mem_index_mode = :sync
|
|
143
|
+
@zero_mem_trace_store = :memory
|
|
144
|
+
@zero_mem_top_k = 5
|
|
145
|
+
@zero_mem_max_top_k = 10
|
|
146
|
+
@zero_mem_local_span = 2
|
|
147
|
+
@zero_mem_window_size = 8
|
|
148
|
+
@zero_mem_window_overlap = 2
|
|
149
|
+
@zero_mem_episode_gap_seconds = 86_400
|
|
150
|
+
@zero_mem_entity_extractor = :heuristic
|
|
151
|
+
@zero_mem_embedding_provider = nil
|
|
152
|
+
@zero_mem_require_local_encoders = false
|
|
153
|
+
@zero_mem_pagerank_damping = 0.6
|
|
154
|
+
@zero_mem_graph_weight = 0.6
|
|
155
|
+
@zero_mem_ner_http_url = "http://127.0.0.1:8765"
|
|
156
|
+
@memory_mode = :classic
|
|
157
|
+
@zero_mem_ttl_days = nil
|
|
158
|
+
@zero_mem_sidecar_timeout_seconds = 5
|
|
159
|
+
@zero_mem_sidecar_failure_threshold = 3
|
|
160
|
+
@zero_mem_shadow_write = false
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def validate!
|
|
164
|
+
mode = @memory_mode.to_sym
|
|
165
|
+
unless ZeroMem::Mode.valid?(mode)
|
|
166
|
+
raise ConfigurationError, "memory_mode must be :classic, :zero_mem, or :hybrid (got #{@memory_mode.inspect})"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
rho = zero_mem_graph_weight.to_f
|
|
170
|
+
gamma = zero_mem_pagerank_damping.to_f
|
|
171
|
+
raise ConfigurationError, "zero_mem_graph_weight must be in [0, 1]" unless rho.between?(0.0, 1.0)
|
|
172
|
+
raise ConfigurationError, "zero_mem_pagerank_damping must be in [0, 1]" unless gamma.between?(0.0, 1.0)
|
|
173
|
+
raise ConfigurationError, "zero_mem_top_k must be positive" unless zero_mem_top_k.to_i.positive?
|
|
174
|
+
raise ConfigurationError, "zero_mem_max_top_k must be >= zero_mem_top_k" if zero_mem_max_top_k.to_i < zero_mem_top_k.to_i
|
|
175
|
+
|
|
176
|
+
extractor = zero_mem_entity_extractor.to_sym
|
|
177
|
+
unless %i[heuristic http].include?(extractor)
|
|
178
|
+
raise ConfigurationError, "zero_mem_entity_extractor must be :heuristic or :http"
|
|
179
|
+
end
|
|
123
180
|
end
|
|
124
181
|
|
|
125
182
|
def consolidation_policy
|
|
@@ -146,6 +203,7 @@ module Llmemory
|
|
|
146
203
|
|
|
147
204
|
def configure
|
|
148
205
|
yield configuration
|
|
206
|
+
configuration.validate!
|
|
149
207
|
end
|
|
150
208
|
|
|
151
209
|
def reset_configuration!
|
|
@@ -16,10 +16,19 @@ module Llmemory
|
|
|
16
16
|
# output_tokens:, total_tokens:
|
|
17
17
|
# memory_write.llmemory memory_type:, user_id:
|
|
18
18
|
# memory_forget.llmemory memory_type:, user_id:, count:
|
|
19
|
-
# retrieve.llmemory query_chars:, candidates:, results:
|
|
19
|
+
# retrieve.llmemory query_chars:, candidates:, results:, zero_mem_compliant:
|
|
20
20
|
# iterative_retrieve.llmemory hops:, total_results:
|
|
21
21
|
# reflect.llmemory window:, insights:
|
|
22
22
|
# mine_skills.llmemory window:, auto_register:
|
|
23
|
+
# trace_write.llmemory user_id:, session_id:, trace_id:, sequence:, maintenance_fanout:
|
|
24
|
+
# zero_mem_state_update.llmemory user_id:, state_key:, trace_id:, supersedes_trace_id:
|
|
25
|
+
# zero_mem_profile.llmemory user_id:, query_chars:
|
|
26
|
+
# zero_mem_hierarchy_retrieve.llmemory user_id:, workload:
|
|
27
|
+
# zero_mem_retrieve.llmemory user_id:, evidence_count:, degraded:
|
|
28
|
+
# zero_mem_graph_retrieve.llmemory user_id:
|
|
29
|
+
# zero_mem_closure.llmemory user_id:, seeds:
|
|
30
|
+
# zero_mem_calibrate.llmemory user_id:, trace_count:
|
|
31
|
+
# encoder_embed.llmemory user_id:, trace_id:, model:, dimensions:, duration_ms:
|
|
23
32
|
module Instrumentation
|
|
24
33
|
module_function
|
|
25
34
|
|
|
@@ -43,7 +43,22 @@ module Llmemory
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def run!
|
|
46
|
-
report = {
|
|
46
|
+
report = {
|
|
47
|
+
consolidated: nil,
|
|
48
|
+
insights: [],
|
|
49
|
+
mined: [],
|
|
50
|
+
expired: { episodic: 0, procedural: 0 },
|
|
51
|
+
errors: {},
|
|
52
|
+
disabled: []
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if @memory&.zero_mem_strict?
|
|
56
|
+
report[:disabled] = %i[consolidate reflect mine]
|
|
57
|
+
report[:consolidated] = false
|
|
58
|
+
step(report, :zero_mem_repair) { report[:zero_mem] = zero_mem_repair } if @memory.trace_store
|
|
59
|
+
step(report, :expire) { report[:expired] = expire } if @expire
|
|
60
|
+
return report
|
|
61
|
+
end
|
|
47
62
|
|
|
48
63
|
step(report, :consolidate) { report[:consolidated] = consolidate } if @memory
|
|
49
64
|
step(report, :reflect) { report[:insights] = reflect } if @reflect
|
|
@@ -79,6 +94,19 @@ module Llmemory
|
|
|
79
94
|
TTLExpiry.run!(@user_id, episodic: episodic, procedural: procedural)
|
|
80
95
|
end
|
|
81
96
|
|
|
97
|
+
def zero_mem_repair
|
|
98
|
+
store = @memory.trace_store
|
|
99
|
+
repair = ZeroMem::Repair.new(storage: store)
|
|
100
|
+
sessions = store.list_traces(@user_id).map(&:session_id).uniq
|
|
101
|
+
sessions.map do |sid|
|
|
102
|
+
repair.run!(
|
|
103
|
+
user_id: @user_id,
|
|
104
|
+
session_id: sid,
|
|
105
|
+
checkpoint_messages: @memory.messages
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
82
110
|
def episodic
|
|
83
111
|
@episodic ||= @memory&.episodic || Llmemory::LongTerm::Episodic::Memory.new(user_id: @user_id)
|
|
84
112
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Llmemory
|
|
4
|
+
module Maintenance
|
|
5
|
+
class ZeroMemTTL
|
|
6
|
+
def initialize(storage:, ttl_days: nil)
|
|
7
|
+
@storage = storage
|
|
8
|
+
@ttl_days = ttl_days || Llmemory.configuration.zero_mem_ttl_days
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def run!(user_id:, dry_run: false)
|
|
12
|
+
return { archived: 0, skipped: true } if @ttl_days.nil? || @ttl_days.to_i <= 0
|
|
13
|
+
|
|
14
|
+
cutoff = Time.now - (@ttl_days.to_i * 86_400)
|
|
15
|
+
archived = 0
|
|
16
|
+
@storage.list_traces(user_id, include_archived: false).each do |trace|
|
|
17
|
+
next unless trace.occurred_at < cutoff
|
|
18
|
+
|
|
19
|
+
archived += 1
|
|
20
|
+
@storage.archive_trace(user_id, trace.id) unless dry_run
|
|
21
|
+
end
|
|
22
|
+
{ archived: archived, dry_run: dry_run, cutoff: cutoff.utc.iso8601 }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/llmemory/maintenance.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative "maintenance/ttl_expiry"
|
|
|
5
5
|
require_relative "maintenance/cognitive_pass"
|
|
6
6
|
require_relative "maintenance/search_tokens_backfill"
|
|
7
7
|
require_relative "maintenance/policy_prune"
|
|
8
|
+
require_relative "maintenance/zero_mem_ttl"
|
|
8
9
|
|
|
9
10
|
module Llmemory
|
|
10
11
|
module Maintenance
|
data/lib/llmemory/mcp/server.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative "authentication"
|
|
|
5
5
|
require_relative "tools/memory_search"
|
|
6
6
|
require_relative "tools/memory_save"
|
|
7
7
|
require_relative "tools/memory_retrieve"
|
|
8
|
+
require_relative "tools/memory_retrieve_evidence"
|
|
8
9
|
require_relative "tools/memory_timeline"
|
|
9
10
|
require_relative "tools/memory_add_message"
|
|
10
11
|
require_relative "tools/memory_consolidate"
|
|
@@ -175,6 +176,7 @@ module Llmemory
|
|
|
175
176
|
Tools::MemorySearch,
|
|
176
177
|
Tools::MemorySave,
|
|
177
178
|
Tools::MemoryRetrieve,
|
|
179
|
+
Tools::MemoryRetrieveEvidence,
|
|
178
180
|
Tools::MemoryTimeline,
|
|
179
181
|
Tools::MemoryTimelineContext,
|
|
180
182
|
Tools::MemoryAddMessage,
|
|
@@ -30,6 +30,22 @@ module Llmemory
|
|
|
30
30
|
def graph_based?
|
|
31
31
|
Llmemory.configuration.long_term_type.to_sym == :graph_based
|
|
32
32
|
end
|
|
33
|
+
|
|
34
|
+
def trace_store_instance
|
|
35
|
+
@trace_store_instance ||= ZeroMem::Storages.build
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def build_memory(user_id:, session_id: "default", trace_store: nil, memory_mode: nil)
|
|
39
|
+
mode = memory_mode || Llmemory.configuration.memory_mode
|
|
40
|
+
store = trace_store
|
|
41
|
+
store = trace_store_instance if store.nil? && ZeroMem::Mode.zero_mem_enabled?(mode)
|
|
42
|
+
Llmemory::Memory.new(
|
|
43
|
+
user_id: user_id,
|
|
44
|
+
session_id: session_id,
|
|
45
|
+
trace_store: store,
|
|
46
|
+
memory_mode: mode
|
|
47
|
+
)
|
|
48
|
+
end
|
|
33
49
|
end
|
|
34
50
|
end
|
|
35
51
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../store_helpers"
|
|
4
|
+
|
|
3
5
|
module Llmemory
|
|
4
6
|
module MCP
|
|
5
7
|
module Tools
|
|
@@ -11,17 +13,28 @@ module Llmemory
|
|
|
11
13
|
user_id: { type: "string", description: "User identifier" },
|
|
12
14
|
session_id: { type: "string", description: "Session identifier (default: 'default')" },
|
|
13
15
|
role: { type: "string", enum: ["user", "assistant", "system", "tool", "tool_result"], description: "Message role" },
|
|
14
|
-
content: { type: "string", description: "Message content" }
|
|
16
|
+
content: { type: "string", description: "Message content" },
|
|
17
|
+
occurred_at: { type: "string", description: "Optional ISO8601 timestamp for trace ingestion" },
|
|
18
|
+
boundary_id: { type: "string", description: "Optional boundary identifier (Zero-Mem)" },
|
|
19
|
+
idempotency_key: { type: "string", description: "Optional idempotency key for trace writes" }
|
|
15
20
|
},
|
|
16
21
|
required: ["user_id", "role", "content"]
|
|
17
22
|
)
|
|
18
23
|
|
|
19
24
|
class << self
|
|
20
|
-
def call(user_id:, role:, content:, session_id: nil,
|
|
25
|
+
def call(user_id:, role:, content:, session_id: nil, occurred_at: nil, boundary_id: nil,
|
|
26
|
+
idempotency_key: nil, server_context: nil)
|
|
21
27
|
session = session_id || "default"
|
|
22
28
|
|
|
23
|
-
memory =
|
|
24
|
-
|
|
29
|
+
memory = StoreHelpers.build_memory(user_id: user_id, session_id: session)
|
|
30
|
+
parsed_time = occurred_at ? Time.parse(occurred_at.to_s) : nil
|
|
31
|
+
memory.add_message(
|
|
32
|
+
role: role.to_sym,
|
|
33
|
+
content: content,
|
|
34
|
+
occurred_at: parsed_time,
|
|
35
|
+
boundary_id: boundary_id,
|
|
36
|
+
idempotency_key: idempotency_key
|
|
37
|
+
)
|
|
25
38
|
|
|
26
39
|
::MCP::Tool::Response.new([{
|
|
27
40
|
type: "text",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../store_helpers"
|
|
4
|
+
|
|
3
5
|
module Llmemory
|
|
4
6
|
module MCP
|
|
5
7
|
module Tools
|
|
@@ -20,7 +22,7 @@ module Llmemory
|
|
|
20
22
|
session = session_id || "default"
|
|
21
23
|
should_clear = clear_session || false
|
|
22
24
|
|
|
23
|
-
memory =
|
|
25
|
+
memory = StoreHelpers.build_memory(user_id: user_id, session_id: session)
|
|
24
26
|
|
|
25
27
|
messages = memory.messages
|
|
26
28
|
if messages.empty?
|
|
@@ -33,9 +35,7 @@ module Llmemory
|
|
|
33
35
|
message_count = messages.size
|
|
34
36
|
memory.consolidate!
|
|
35
37
|
|
|
36
|
-
if should_clear
|
|
37
|
-
memory.clear_session!
|
|
38
|
-
end
|
|
38
|
+
memory.clear_session! if should_clear
|
|
39
39
|
|
|
40
40
|
response_text = "Consolidated #{message_count} messages from session '#{session}' into long-term memory."
|
|
41
41
|
response_text += "\nSession cleared." if should_clear
|
|
@@ -44,6 +44,11 @@ module Llmemory
|
|
|
44
44
|
type: "text",
|
|
45
45
|
text: response_text
|
|
46
46
|
}])
|
|
47
|
+
rescue Llmemory::GenerativeOperationDisabled => e
|
|
48
|
+
::MCP::Tool::Response.new([{
|
|
49
|
+
type: "text",
|
|
50
|
+
text: e.message
|
|
51
|
+
}], error: true)
|
|
47
52
|
rescue => e
|
|
48
53
|
::MCP::Tool::Response.new([{
|
|
49
54
|
type: "text",
|