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/file.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 (automatic) —

When a Memory object is passed to an agent, the agent automatically saves each successful turn (request + response) after the call.

memory = ActiveHarness::Memory.new(session_id: "u42", depth: 8)
SupportAgent.call(input: "Hello", memory: memory)
# => turn is saved to storage/ai/memory/u42.json

— 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

Direct Known Subclasses

TestSupportMemory

Defined Under Namespace

Modules: Adapter

Constant Summary collapse

ADAPTERS =
{
  file: ->(**opts) { Adapter::File.new(**opts) }
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(session_id:, depth: nil, adapter: :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          — :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.)


74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/active_harness/memory.rb', line 74

def initialize(
  session_id:,
  depth:       nil,
  adapter:     :file,
  enabled:     true,
  read_only:   false,
  namespace:   nil,
  on_trim:     nil,
  async:       false,
  **adapter_opts
)
  @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.



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

def session_id
  @session_id
end

Instance Method Details

#clearObject

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



166
167
168
169
# File 'lib/active_harness/memory.rb', line 166

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

#closeObject

Flush and close the adapter.



181
182
183
# File 'lib/active_harness/memory.rb', line 181

def close
  @adapter.close
end

#deleteObject

Delete the session from storage backend entirely.



172
173
174
175
176
177
178
# File 'lib/active_harness/memory.rb', line 172

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.



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

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.



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/active_harness/memory.rb', line 118

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.



161
162
163
# File 'lib/active_harness/memory.rb', line 161

def size
  @turns.size
end

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

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

filter: ->(turn) { turn[:agent] == "SupportAgent" }
since:  Time.now - 3600


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

def to_messages(filter: nil, since: 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.flat_map do |t|
    [
      { role: "user",      content: t[:request]  },
      { role: "assistant", content: t[:response] }
    ]
  end
end

#turnsObject

All stored turns without depth/filter trimming.



156
157
158
# File 'lib/active_harness/memory.rb', line 156

def turns
  @turns.dup
end