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) ⇒ Memory

Returns a new instance of Memory.



13
14
15
16
17
18
19
20
21
22
23
24
# 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)
  @user_id = user_id
  @session_id = session_id
  @checkpoint = checkpoint || ShortTerm::Checkpoint.new(user_id: user_id, session_id: session_id)
  @working_memory = working_memory
  @episodic = episodic
  @procedural = procedural
  @llm = api_key.to_s.empty? ? nil : Llmemory::LLM.client(api_key: api_key)
  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: @llm)
end

Instance Method Details

#add_message(role:, content:) ⇒ Object



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

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



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/llmemory/memory.rb', line 204

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



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

def clear_session!
  @checkpoint.clear_state
  true
end

#compact!(max_bytes: nil) ⇒ Object



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

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



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

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



169
170
171
# File 'lib/llmemory/memory.rb', line 169

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.



34
35
36
# File 'lib/llmemory/memory.rb', line 34

def episodic
  @episodic ||= LongTerm::Episodic::Memory.new(user_id: @user_id)
end

#last_user_messageObject



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

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

#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.



68
69
70
71
72
73
74
# File 'lib/llmemory/memory.rb', line 68

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

#maybe_flush_memory!Object



160
161
162
163
164
165
166
167
# File 'lib/llmemory/memory.rb', line 160

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



83
84
85
86
87
88
89
# File 'lib/llmemory/memory.rb', line 83

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.



60
61
62
63
# File 'lib/llmemory/memory.rb', line 60

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

#proceduralObject

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



39
40
41
# File 'lib/llmemory/memory.rb', line 39

def procedural
  @procedural ||= LongTerm::Procedural::Memory.new(user_id: @user_id)
end

#prune!(mode: nil) ⇒ Object



113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/llmemory/memory.rb', line 113

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.



52
53
54
# File 'lib/llmemory/memory.rb', line 52

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

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



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

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.



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

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

#retrieve(query, max_tokens: nil) ⇒ Object



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

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)


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

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)


179
180
181
182
183
# File 'lib/llmemory/memory.rb', line 179

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

#user_idObject



221
222
223
# File 'lib/llmemory/memory.rb', line 221

def user_id
  @user_id
end

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



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/llmemory/memory.rb', line 185

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.



28
29
30
# File 'lib/llmemory/memory.rb', line 28

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