Class: HTM::WorkingMemory

Inherits:
Object
  • Object
show all
Defined in:
lib/htm/working_memory.rb

Overview

Working Memory - Token-limited active context for immediate LLM use

WorkingMemory manages the active conversation context within token limits. When full, it evicts less important or older nodes back to long-term storage.

Thread Safety: All public methods are protected by a mutex to ensure safe concurrent access from multiple threads.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(max_tokens:) ⇒ WorkingMemory

Initialize working memory

Parameters:

  • max_tokens (Integer)

    Maximum tokens allowed in working memory



19
20
21
22
23
24
# File 'lib/htm/working_memory.rb', line 19

def initialize(max_tokens:)
  @max_tokens = max_tokens
  @nodes = {}
  @access_order = []
  @mutex = Mutex.new
end

Instance Attribute Details

#max_tokensObject (readonly)

Returns the value of attribute max_tokens.



13
14
15
# File 'lib/htm/working_memory.rb', line 13

def max_tokens
  @max_tokens
end

Instance Method Details

#add(key, value, token_count:, access_count: 0, last_accessed: nil, from_recall: false) ⇒ void

This method returns an undefined value.

Add a node to working memory

Parameters:

  • key (String)

    Node identifier

  • value (String)

    Node content

  • token_count (Integer)

    Number of tokens in this node

  • access_count (Integer) (defaults to: 0)

    Access count from long-term memory (default: 0)

  • last_accessed (Time, nil) (defaults to: nil)

    Last access time from long-term memory

  • from_recall (Boolean) (defaults to: false)

    Whether this node was recalled from long-term memory



36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/htm/working_memory.rb', line 36

def add(key, value, token_count:, access_count: 0, last_accessed: nil, from_recall: false)
  @mutex.synchronize do
    @nodes[key] = {
      value: value,
      token_count: token_count,
      access_count: access_count,
      last_accessed: last_accessed || Time.now,
      added_at: Time.now,
      from_recall: from_recall
    }
    update_access_unlocked(key)
  end
end

#add_from_sync(id:, content:, token_count:, created_at:) ⇒ void

This method returns an undefined value.

Add a node from sync notification (bypasses normal add flow)

Called by RobotGroup when another robot adds to working memory. Does not trigger notifications to avoid infinite loops.

Parameters:

  • id (Integer)

    Node database ID

  • content (String)

    Node content

  • token_count (Integer)

    Token count

  • created_at (Time)

    When node was created



195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/htm/working_memory.rb', line 195

def add_from_sync(id:, content:, token_count:, created_at:)
  @mutex.synchronize do
    key = id.to_s
    return if @nodes.key?(key)  # Already have this node

    @nodes[key] = {
      value: content,
      token_count: token_count,
      access_count: 0,
      last_accessed: Time.now,
      added_at: created_at,
      from_recall: false,
      from_sync: true
    }
    update_access_unlocked(key)
  end
end

#assemble_context(strategy:, max_tokens: nil) ⇒ String

Assemble context string for LLM

Parameters:

  • strategy (Symbol)

    Assembly strategy (:recent, :frequent, :balanced)

    • :recent - Most recently accessed (LRU)

    • :frequent - Most frequently accessed (LFU)

    • :balanced - Combines frequency × recency

  • max_tokens (Integer, nil) (defaults to: nil)

    Optional token limit

Returns:

  • (String)

    Assembled context



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

def assemble_context(strategy:, max_tokens: nil)
  @mutex.synchronize do
    max = max_tokens || @max_tokens
    nodes = sorted_nodes_by_strategy(strategy)

    context_parts = []
    current_tokens = 0
    nodes.each do |node|
      break if current_tokens + node[:token_count] > max
      context_parts << node[:value]
      current_tokens += node[:token_count]
    end

    context_parts.join("\n\n")
  end
end

#clearvoid

This method returns an undefined value.

Clear all nodes from working memory



173
174
175
176
177
178
# File 'lib/htm/working_memory.rb', line 173

def clear
  @mutex.synchronize do
    @nodes.clear
    @access_order.clear
  end
end

#clear_from_syncvoid

This method returns an undefined value.

Clear all nodes from sync notification

Called by RobotGroup when another robot clears working memory.



234
235
236
237
238
239
# File 'lib/htm/working_memory.rb', line 234

def clear_from_sync
  @mutex.synchronize do
    @nodes.clear
    @access_order.clear
  end
end

#evict_to_make_space(needed_tokens) ⇒ Array<Hash>

Evict nodes to make space

Uses LFU + LRU strategy: Least Frequently Used + Least Recently Used Nodes with low access count and old timestamps are evicted first

Parameters:

  • needed_tokens (Integer)

    Number of tokens needed

Returns:

  • (Array<Hash>)

    Evicted nodes



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/htm/working_memory.rb', line 81

def evict_to_make_space(needed_tokens)
  @mutex.synchronize do
    evicted = []
    tokens_freed = 0

    # Sort by access frequency + recency (lower score = more evictable)
    candidates = @nodes.sort_by do |_key, node|
      access_frequency = node[:access_count] || 0
      time_since_accessed = Time.now - (node[:last_accessed] || node[:added_at])

      # Combined score: lower is more evictable
      # Frequently accessed = higher score (keep)
      # Recently accessed = higher score (keep)
      access_score = Math.log(1 + access_frequency)
      recency_score = 1.0 / (1 + (time_since_accessed / 3600.0))

      -(access_score + recency_score)  # Negative for ascending sort
    end

    candidates.each do |key, node|
      break if tokens_freed >= needed_tokens

      evicted << { key: key, value: node[:value] }
      tokens_freed += node[:token_count]
      @nodes.delete(key)
      @access_order.delete(key)
    end

    evicted
  end
end

#has_space?(token_count) ⇒ Boolean

Check if there’s space for a node

Parameters:

  • token_count (Integer)

    Number of tokens needed

Returns:

  • (Boolean)

    true if space available



67
68
69
70
71
# File 'lib/htm/working_memory.rb', line 67

def has_space?(token_count)
  @mutex.synchronize do
    current_tokens_unlocked + token_count <= @max_tokens
  end
end

#node_countInteger

Get node count

Returns:

  • (Integer)

    Number of nodes in working memory



163
164
165
166
167
# File 'lib/htm/working_memory.rb', line 163

def node_count
  @mutex.synchronize do
    @nodes.size
  end
end

#remove(key) ⇒ void

This method returns an undefined value.

Remove a node from working memory

Parameters:

  • key (String)

    Node identifier



55
56
57
58
59
60
# File 'lib/htm/working_memory.rb', line 55

def remove(key)
  @mutex.synchronize do
    @nodes.delete(key)
    @access_order.delete(key)
  end
end

#remove_from_sync(node_id) ⇒ void

This method returns an undefined value.

Remove a node from sync notification

Called by RobotGroup when another robot evicts from working memory.

Parameters:

  • node_id (Integer)

    Node database ID



220
221
222
223
224
225
226
# File 'lib/htm/working_memory.rb', line 220

def remove_from_sync(node_id)
  @mutex.synchronize do
    key = node_id.to_s
    @nodes.delete(key)
    @access_order.delete(key)
  end
end

#token_countInteger

Get current token count

Returns:

  • (Integer)

    Total tokens in working memory



143
144
145
146
147
# File 'lib/htm/working_memory.rb', line 143

def token_count
  @mutex.synchronize do
    current_tokens_unlocked
  end
end

#utilization_percentageFloat

Get utilization percentage

Returns:

  • (Float)

    Percentage of working memory used



153
154
155
156
157
# File 'lib/htm/working_memory.rb', line 153

def utilization_percentage
  @mutex.synchronize do
    (current_tokens_unlocked.to_f / @max_tokens * 100).round(2)
  end
end