Class: Llmemory::Memory

Inherits:
Object
  • Object
show all
Defined in:
lib/llmemory/memory.rb

Constant Summary collapse

DEFAULT_SESSION_ID =
"default"
STATE_KEY_MESSAGES =
:messages

Instance Method Summary collapse

Constructor Details

#initialize(user_id:, session_id: DEFAULT_SESSION_ID, checkpoint: nil, long_term: nil, long_term_type: nil, retrieval_engine: nil, working_memory: nil, episodic: nil, procedural: nil, api_key: nil, encryption_key: :inherit) ⇒ Memory

Returns a new instance of Memory.



13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/llmemory/memory.rb', line 13

def initialize(user_id:, session_id: DEFAULT_SESSION_ID, checkpoint: nil, long_term: nil, long_term_type: nil, retrieval_engine: nil, working_memory: nil, episodic: nil, procedural: nil, api_key: nil, encryption_key: :inherit)
  @user_id = user_id
  @session_id = session_id
  resolved_key = encryption_key == :inherit ? nil : encryption_key
  @cipher = Llmemory.build_cipher(resolved_key)
  if checkpoint
    @checkpoint = checkpoint
    @short_term_store = checkpoint.store
  else
    @short_term_store = build_short_term_store(@cipher)
    @checkpoint = ShortTerm::Checkpoint.new(
      user_id: user_id,
      session_id: session_id,
      store: @short_term_store,
      cipher: @cipher
    )
  end
  @working_memory = working_memory
  @episodic = episodic
  @procedural = procedural
  @api_key = api_key unless api_key.to_s.empty?
  type = long_term_type || Llmemory.configuration.long_term_type || :file_based
  @long_term = long_term || build_long_term(type)
  @retrieval_engine = retrieval_engine || Retrieval::Engine.new(
    Retrieval::MultiSource.new(primary: @long_term, memory: self),
    llm: tracked_llm_client,
    feedback: Retrieval::FeedbackStore.new(store: @short_term_store)
  )
end

Instance Method Details

#add_message(role:, content:) ⇒ Object



107
108
109
110
111
112
113
114
115
116
117
# File 'lib/llmemory/memory.rb', line 107

def add_message(role:, content:)
  @short_term_store.update(@user_id, @session_id) do |state|
    state = normalize_state_hash(state)
    list = state[STATE_KEY_MESSAGES]
    list = list.is_a?(Array) ? list.dup : []
    list << { role: role.to_sym, content: content.to_s }
    list = sanitize_messages(list) if Llmemory.configuration.message_sanitizer_enabled
    state.merge(STATE_KEY_MESSAGES => list, last_activity_at: Time.now, **preserved_flush_state_from(state))
  end
  true
end

#check_context_window!Object



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/llmemory/memory.rb', line 241

def check_context_window!
  return false if messages.empty?

  flushed = false
  if should_auto_consolidate? && Llmemory.configuration.memory_flush_enabled
    consolidate!
    flushed = true
  end

  compacted = false
  if should_compact?
    compacted = compact!
  end

  flushed || compacted
end

#clear_session!Object



172
173
174
175
176
# File 'lib/llmemory/memory.rb', line 172

def clear_session!
  @checkpoint.clear_state
  working_memory.clear!
  true
end

#compact!(max_bytes: nil) ⇒ Object



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/llmemory/memory.rb', line 178

def compact!(max_bytes: nil)
  max = max_bytes || Llmemory.configuration.compact_max_bytes
  msgs = messages
  current_bytes = messages_byte_size(msgs)
  return false if current_bytes <= max

  flushed = flush_memory_before_compaction!(msgs)

  old_msgs, recent_msgs = split_messages_by_bytes(msgs, max)
  return false if old_msgs.empty?

  summary = summarize_messages(old_msgs)
  compacted = [{ role: :system, content: summary }] + recent_msgs
  state = restore_state_for_save
  flush_ts = flushed ? Time.now : (state[:last_flush_at] || state["last_flush_at"])
  save_state(messages: compacted, last_compact_at: Time.now, last_flush_at: flush_ts)
  true
end

#consolidate!Object



164
165
166
167
168
169
170
# File 'lib/llmemory/memory.rb', line 164

def consolidate!
  msgs = messages
  return true if msgs.empty?
  conversation_text = msgs.map { |m| format_message(m) }.join("\n")
  @long_term.memorize(conversation_text)
  true
end

#context_tokensObject



206
207
208
# File 'lib/llmemory/memory.rb', line 206

def context_tokens
  estimated_tokens(messages)
end

#episodicObject

Episodic long-term memory (CoALA): records and retrieves agent trajectories. Additive — coexists with the semantic store (file/graph). Lazily built.



55
56
57
58
59
60
61
62
# File 'lib/llmemory/memory.rb', line 55

def episodic
  @episodic ||= LongTerm::Episodic::Memory.new(
    user_id: @user_id,
    storage: LongTerm::Episodic::Storages.build(cipher: @cipher),
    cipher: @cipher,
    forget_log_store: @short_term_store
  )
end

#last_user_messageObject



143
144
145
146
147
# File 'lib/llmemory/memory.rb', line 143

def last_user_message
  msgs = messages
  idx = msgs.rindex { |m| (m[:role] || m["role"]).to_s == "user" }
  idx ? (msgs[idx][:content] || msgs[idx]["content"]).to_s : ""
end

#llm_usageObject



262
263
264
# File 'lib/llmemory/memory.rb', line 262

def llm_usage
  Llmemory::LLM::UsageLedger.new(store: @short_term_store).totals(@user_id)
end

#maintain!(**opts) ⇒ Object

Cognitive maintenance pass: consolidate -> reflect -> mine skills -> expire, in one step, closing the CoALA learning loop. Each step is isolated; a failure in one is captured in the report and never aborts the others.



99
100
101
102
103
104
105
# File 'lib/llmemory/memory.rb', line 99

def maintain!(**opts)
  Maintenance::CognitivePass.run!(
    @user_id,
    memory: self, episodic: episodic, procedural: procedural, semantic: @long_term, llm: tracked_llm_client,
    **opts
  )
end

#maybe_flush_memory!Object



197
198
199
200
201
202
203
204
# File 'lib/llmemory/memory.rb', line 197

def maybe_flush_memory!
  return false unless Llmemory.configuration.memory_flush_enabled
  msgs = messages
  return false if msgs.empty?
  return false if estimated_tokens(msgs) < Llmemory.configuration.memory_flush_threshold_tokens

  consolidate!
end

#messagesObject



119
120
121
122
123
124
125
# File 'lib/llmemory/memory.rb', line 119

def messages
  state = @checkpoint.restore_state
  return [] unless state.is_a?(Hash)
  list = state[STATE_KEY_MESSAGES] || state[STATE_KEY_MESSAGES.to_s]
  list = list.is_a?(Array) ? list.dup : []
  sanitize_messages(list)
end

#mine_skills!(window: SkillMining::Miner::DEFAULT_WINDOW, outcomes: nil, auto_register: false) ⇒ Object

Mines recent episodes for reusable skills (Voyager-style). Human-in-the-loop by default: returns skill proposals and writes nothing. With auto_register: true, registers them in procedural memory (with provenance back to the source episodes) and returns the new skill ids.



91
92
93
94
# File 'lib/llmemory/memory.rb', line 91

def mine_skills!(window: SkillMining::Miner::DEFAULT_WINDOW, outcomes: nil, auto_register: false)
  SkillMining::Miner.new(episodic: episodic, procedural: procedural, llm: tracked_llm_client)
    .mine(window: window, outcomes: outcomes, auto_register: auto_register)
end

#proceduralObject

Procedural long-term memory (Voyager-style skill library). Lazily built.



65
66
67
68
69
70
71
72
# File 'lib/llmemory/memory.rb', line 65

def procedural
  @procedural ||= LongTerm::Procedural::Memory.new(
    user_id: @user_id,
    storage: LongTerm::Procedural::Storages.build(cipher: @cipher),
    cipher: @cipher,
    forget_log_store: @short_term_store
  )
end

#prune!(mode: nil) ⇒ Object



149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/llmemory/memory.rb', line 149

def prune!(mode: nil)
  return false unless Llmemory.configuration.prune_tool_results_enabled

  msgs = messages
  return false if msgs.empty?

  mode ||= Llmemory.configuration.prune_tool_results_mode
  pruner = ShortTerm::Pruner.new(
    soft_trim_max_bytes: Llmemory.configuration.prune_tool_results_max_bytes
  )
  pruned = pruner.prune!(msgs, mode: mode)
  save_state(messages: pruned, **preserved_flush_state)
  true
end

#reason(template:, into: Actions::Reason::DEFAULT_SLOT, parse: nil) ⇒ Object

Reasoning action: render a prompt from working memory, call the LLM, write the result back. Composable; does not touch long-term memory.



83
84
85
# File 'lib/llmemory/memory.rb', line 83

def reason(template:, into: Actions::Reason::DEFAULT_SLOT, parse: nil)
  Actions::Reason.call(working_memory: working_memory, template: template, into: into, parse: parse, llm: tracked_llm_client)
end

#recall_for(query: nil, max_tokens: nil) ⇒ Object



134
135
136
137
138
139
140
141
# File 'lib/llmemory/memory.rb', line 134

def recall_for(query: nil, max_tokens: nil)
  return "" unless Llmemory.configuration.auto_recall_enabled

  effective_query = query || last_user_message
  return "" if effective_query.to_s.strip.empty?

  retrieve(effective_query, max_tokens: max_tokens)
end

#reflect!(window: 10, category: "insights") ⇒ Object

Reflects over recent episodes and writes distilled insights to the semantic store (file/graph) with provenance back to source episodes.



76
77
78
79
# File 'lib/llmemory/memory.rb', line 76

def reflect!(window: 10, category: "insights")
  Reflection::Reflector.new(episodic: episodic, semantic: @long_term, llm: tracked_llm_client)
    .reflect(window: window, category: category)
end

#retrieve(query, max_tokens: nil) ⇒ Object



127
128
129
130
131
132
# File 'lib/llmemory/memory.rb', line 127

def retrieve(query, max_tokens: nil)
  msgs = pruned_messages
  short_context = format_short_term_context(msgs)
  long_context = @retrieval_engine.retrieve_for_inference(query, user_id: @user_id, max_tokens: max_tokens)
  combine_contexts(short_context, long_context)
end

#should_auto_consolidate?Boolean

Returns:

  • (Boolean)


210
211
212
213
214
# File 'lib/llmemory/memory.rb', line 210

def should_auto_consolidate?
  ctx = context_tokens
  threshold = Llmemory.configuration.context_window_tokens - Llmemory.configuration.reserve_tokens
  ctx >= threshold
end

#should_compact?Boolean

Returns:

  • (Boolean)


216
217
218
219
220
# File 'lib/llmemory/memory.rb', line 216

def should_compact?
  ctx = context_tokens
  threshold = Llmemory.configuration.context_window_tokens - Llmemory.configuration.reserve_tokens
  ctx >= threshold
end

#user_idObject



258
259
260
# File 'lib/llmemory/memory.rb', line 258

def user_id
  @user_id
end

#with_overflow_recovery(max_retries: 2, &block) ⇒ Object



222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/llmemory/memory.rb', line 222

def with_overflow_recovery(max_retries: 2, &block)
  return yield unless Llmemory.configuration.overflow_recovery_enabled
  return yield unless block_given?

  retries = 0
  begin
    yield
  rescue Llmemory::LLMError => e
    msg = e.message.to_s.downcase
    overflow = msg.include?("context") || msg.include?("token") || msg.include?("overflow") || msg.include?("limit")
    raise unless overflow && retries < max_retries

    prune! if Llmemory.configuration.prune_tool_results_enabled
    compact!
    retries += 1
    retry
  end
end

#working_memoryObject

Structured working memory for this session (CoALA working memory), parallel to the message checkpoint. Lazily built.



45
46
47
48
49
50
51
# File 'lib/llmemory/memory.rb', line 45

def working_memory
  @working_memory ||= WorkingMemory.new(
    user_id: @user_id,
    session_id: @session_id,
    store: @short_term_store
  )
end