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



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

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



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

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



116
117
118
119
# File 'lib/llmemory/memory.rb', line 116

def clear_session!
  @checkpoint.clear_state
  true
end

#compact!(max_bytes: nil) ⇒ Object



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/llmemory/memory.rb', line 121

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



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

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



149
150
151
# File 'lib/llmemory/memory.rb', line 149

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



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

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

#maybe_flush_memory!Object



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

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



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

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

#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



93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/llmemory/memory.rb', line 93

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



78
79
80
81
82
83
84
85
# File 'lib/llmemory/memory.rb', line 78

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



71
72
73
74
75
76
# File 'lib/llmemory/memory.rb', line 71

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)


153
154
155
156
157
# File 'lib/llmemory/memory.rb', line 153

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)


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

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

#user_idObject



201
202
203
# File 'lib/llmemory/memory.rb', line 201

def user_id
  @user_id
end

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



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/llmemory/memory.rb', line 165

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