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(
    @long_term,
    llm: tracked_llm_client,
    feedback: Retrieval::FeedbackStore.new(store: @short_term_store)
  )
end

Instance Method Details

#add_message(role:, content:) ⇒ Object



105
106
107
108
109
110
# File 'lib/llmemory/memory.rb', line 105

def add_message(role:, content:)
  msgs = messages
  msgs << { role: role.to_sym, content: content.to_s }
  save_state(messages: msgs, **preserved_flush_state)
  true
end

#check_context_window!Object



233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/llmemory/memory.rb', line 233

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



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

def clear_session!
  @checkpoint.clear_state
  true
end

#compact!(max_bytes: nil) ⇒ Object



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/llmemory/memory.rb', line 170

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



157
158
159
160
161
162
163
# File 'lib/llmemory/memory.rb', line 157

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



198
199
200
# File 'lib/llmemory/memory.rb', line 198

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
# 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
  )
end

#last_user_messageObject



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

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



254
255
256
# File 'lib/llmemory/memory.rb', line 254

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.



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

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



189
190
191
192
193
194
195
196
# File 'lib/llmemory/memory.rb', line 189

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



112
113
114
115
116
117
118
# File 'lib/llmemory/memory.rb', line 112

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.



89
90
91
92
# File 'lib/llmemory/memory.rb', line 89

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.



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

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

#prune!(mode: nil) ⇒ Object



142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/llmemory/memory.rb', line 142

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.



81
82
83
# File 'lib/llmemory/memory.rb', line 81

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



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

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.



74
75
76
77
# File 'lib/llmemory/memory.rb', line 74

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



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

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)


202
203
204
205
206
# File 'lib/llmemory/memory.rb', line 202

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)


208
209
210
211
212
# File 'lib/llmemory/memory.rb', line 208

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

#user_idObject



250
251
252
# File 'lib/llmemory/memory.rb', line 250

def user_id
  @user_id
end

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



214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/llmemory/memory.rb', line 214

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: build_short_term_store(@cipher)
  )
end