Class: ActiveHarness::Memory

Inherits:
Object
  • Object
show all
Defined in:
lib/active_harness/memory.rb,
lib/active_harness/memory/adapter/base.rb,
lib/active_harness/memory/adapter/sqlite.rb,
lib/active_harness/memory/adapter/json_file.rb,
lib/active_harness/memory/adapter/postgresql.rb

Overview

Conversational memory for agents.

Memory only records the history of request/response turns. It does NOT automatically inject history into LLM messages. Injection is always manual — you control when and how context is used.

--- Recording ---

Passing a Memory object to Agent.call(memory:) does NOT by itself save anything — Agent never calls #load or #record on it. Loading and recording turns for a bare agent call is entirely manual (e.g. via before_call/after_call hooks calling @memory.load / @memory.record).

Pipeline is the one place recording is automatic: when a Memory is passed to Pipeline#call, the pipeline itself calls #load before running steps and #record after a successful run — independent of any hooks the individual agents define.

memory = ActiveHarness::Memory.new(session_id: "u42", depth: 8)
SupportAgent.call(input: "Hello", memory: memory)
# => nothing is saved unless SupportAgent's own hooks record it

--- Manual injection patterns ---

Option A: prepend history to input in a before_call hook

on :before_call do
history = @memory&.to_messages
if history&.any?
  lines = history.map { |m| "#{m[:role]}: #{m[:content]}" }.join("\n")
  @input = "Previous conversation:\n#{lines}\n\nUser: #{@input}"
end
end

Option B: inject history into the system prompt

on :after_system_prompt do |prompt|
history = @memory&.to_messages
if history&.any?
  lines = history.map { |m| "#{m[:role]}: #{m[:content]}" }.join("\n")
  @system_prompt = "#{prompt}\n\nConversation so far:\n#{lines}"
end
end

Option C: use a prompt class that reads @memory directly

class SupportPrompt
def call
  base = "You are a helpful assistant."
  return base unless @memory&.size&.positive?
  history = @memory.to_messages
                   .map { |m| "#{m[:role]}: #{m[:content]}" }
                   .join("\n")
  "#{base}\n\nConversation so far:\n#{history}"
end
end

Defined Under Namespace

Modules: Adapter Classes: JsonFile, Postgresql, Sqlite

Constant Summary collapse

ADAPTERS =
{
  json_file: ->(**opts) { Adapter::JsonFile.new(**opts) }
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(session_id:, depth: nil, adapter: :json_file, enabled: true, read_only: false, namespace: nil, on_trim: nil, async: false, **adapter_opts) ⇒ Memory


Constructor

session_id — required; uniquely identifies this conversation depth — how many past turns to inject into messages (nil = all) adapter — :json_file (default), or an adapter instance enabled — false disables all reads and writes (no-op mode) read_only — true: load history but never write new turns namespace — isolates history per-agent within a session on_trim — Proc called with trimmed turns on storage trim async — write to adapter in a background thread **adapter_opts — forwarded to the adapter (path, storage_size, etc.)



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
112
113
# File 'lib/active_harness/memory.rb', line 83

def initialize(
  session_id:,
  depth:       nil,
  adapter:     :json_file,
  enabled:     true,
  read_only:   false,
  namespace:   nil,
  on_trim:     nil,
  async:       false,
  **adapter_opts
)
  if self.class == Memory
    raise NotImplementedError,
      "Memory cannot be instantiated directly. " \
      "Use Memory::JsonFile, Memory::Postgresql, or Memory::Sqlite."
  end

  @session_id  = session_id
  @depth       = depth
  @enabled     = enabled
  @read_only   = read_only
  @namespace   = namespace
  @async       = async
  @turns       = []
  @loaded      = false

  adapter_opts[:namespace] = namespace if namespace
  adapter_opts[:on_trim]   = on_trim   if on_trim

  @adapter = resolve_adapter(adapter, adapter_opts)
end

Instance Attribute Details

#session_idObject (readonly)

Returns the value of attribute session_id.



69
70
71
# File 'lib/active_harness/memory.rb', line 69

def session_id
  @session_id
end

Instance Method Details

#clearObject

Clear in-RAM turns (does not touch the storage file/key).



183
184
185
186
# File 'lib/active_harness/memory.rb', line 183

def clear
  @turns  = []
  @loaded = false
end

#closeObject

Flush and close the adapter.



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

def close
  @adapter.close
end

#deleteObject

Delete the session from storage backend entirely.



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

def delete
  return unless @enabled

  @adapter.open(@session_id) unless @loaded
  @adapter.delete
  clear
end

#loadObject

Load history from storage into RAM. Called automatically by the agent at the start of #call. After loading, history is available via #turns and #to_messages for manual injection in hooks or prompt classes.



123
124
125
126
127
128
129
130
# File 'lib/active_harness/memory.rb', line 123

def load
  return unless @enabled
  return if @loaded

  @adapter.open(@session_id)
  @turns  = @adapter.read
  @loaded = true
end

#record(request:, response:, **meta) ⇒ Object

Record a turn after a successful agent call.



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/active_harness/memory.rb', line 133

def record(request:, response:, **meta)
  return unless @enabled
  return if @read_only

  turn = { request: request.to_s, response: response.to_s }
  turn.merge!(meta) unless meta.empty?
  turn[:at] ||= Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")

  @turns << turn

  if @async
    Thread.new { safe_write(turn) }
  else
    safe_write(turn)
    # keep RAM in sync with what adapter stored (adapter may have trimmed)
    @turns = @adapter.read
  end
end

#sizeObject

Number of turns currently in memory.



178
179
180
# File 'lib/active_harness/memory.rb', line 178

def size
  @turns.size
end

#to_messages(filter: nil, since: nil, token_budget: nil) ⇒ Object

Returns messages array for LLM consumption, respecting depth. Optional filters:

filter:       ->(turn) { turn[:agent] == "SupportAgent" }
since:        Time.now - 3600
token_budget: 4000  # rough limit (chars / 4 estimate); trims oldest turns first


157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/active_harness/memory.rb', line 157

def to_messages(filter: nil, since: nil, token_budget: nil)
  turns = @turns.dup
  turns.select! { |t| filter.call(t) }   if filter
  turns.select! { |t| after?(t, since) } if since
  turns = turns.last(@depth)              if @depth
  turns = trim_to_token_budget(turns, token_budget) if token_budget

  turns.flat_map do |t|
    [
      { role: "user",      content: t[:request]  },
      { role: "assistant", content: t[:response] }
    ]
  end
end

#turnsObject

All stored turns without depth/filter trimming.



173
174
175
# File 'lib/active_harness/memory.rb', line 173

def turns
  @turns.dup
end