Class: Ask::Agent::Memory
- Inherits:
-
Object
- Object
- Ask::Agent::Memory
- Defined in:
- lib/ask/agent/memory.rb
Overview
Durable, namespaced memory on any State::Adapter — the same storage layer as sessions and checkpoints.
Entries are plain facts ("the deploy window is Tuesday", "the user prefers concise answers") that outlive a session: session A writes them, session B (same adapter + namespace) retrieves them via keyword search and has them injected into context. The abstraction is domain-agnostic — nothing here assumes a coding agent.
Storage shape (pure KV, no list primitives — works with every backend including custom get/set/delete adapters):
memory:<namespace>:<id> — one key per entry
memory:<namespace>:index — JSON array of entry ids (write order)
store = Ask::State::Providers::SQLite.new(path: "agent.db")
memory = Ask::Agent::Memory.new(state: store, namespace: "user:42")
memory.write("Deploy window is Tuesday")
memory.search("when can we deploy?") # => [Entry]
Namespaces isolate memory: a support agent's facts never leak into a finance agent's, and tenants share one backend safely.
Defined Under Namespace
Classes: Entry
Constant Summary collapse
- KEY_PREFIX =
"memory:"- INDEX_SUFFIX =
":index"
Instance Attribute Summary collapse
-
#namespace ⇒ String
readonly
The namespace this memory is scoped to.
-
#state ⇒ Ask::State::Adapter
readonly
The underlying adapter.
Instance Method Summary collapse
-
#count ⇒ Integer
Number of entries in this namespace.
-
#delete(id) ⇒ void
Remove an entry by id.
-
#initialize(state:, namespace:, max_entries: nil) ⇒ Memory
constructor
A new instance of Memory.
-
#list(limit: 50) ⇒ Array<Entry>
Entries, newest first.
-
#search(query, limit: 5) ⇒ Array<Entry>
Keyword search over the namespace's entries: entries matching any query term (case-insensitive substring), ranked by matched-term count, newest first on ties.
-
#write(content, metadata: {}) ⇒ Entry
Save a fact.
Constructor Details
#initialize(state:, namespace:, max_entries: nil) ⇒ Memory
Returns a new instance of Memory.
43 44 45 46 47 48 |
# File 'lib/ask/agent/memory.rb', line 43 def initialize(state:, namespace:, max_entries: nil) @state = state @namespace = namespace.to_s @max_entries = max_entries @mutex = Monitor.new end |
Instance Attribute Details
#namespace ⇒ String (readonly)
Returns the namespace this memory is scoped to.
54 55 56 |
# File 'lib/ask/agent/memory.rb', line 54 def namespace @namespace end |
#state ⇒ Ask::State::Adapter (readonly)
Returns the underlying adapter.
51 52 53 |
# File 'lib/ask/agent/memory.rb', line 51 def state @state end |
Instance Method Details
#count ⇒ Integer
Returns number of entries in this namespace.
117 118 119 |
# File 'lib/ask/agent/memory.rb', line 117 def count entries.size end |
#delete(id) ⇒ void
This method returns an undefined value.
Remove an entry by id.
108 109 110 111 112 113 114 |
# File 'lib/ask/agent/memory.rb', line 108 def delete(id) @mutex.synchronize do @state.delete(entry_key(id)) @state.set(index_key, (load_index - [id]).to_json) end nil end |
#list(limit: 50) ⇒ Array<Entry>
Returns entries, newest first.
100 101 102 |
# File 'lib/ask/agent/memory.rb', line 100 def list(limit: 50) entries.last(limit).reverse end |
#search(query, limit: 5) ⇒ Array<Entry>
Keyword search over the namespace's entries: entries matching any query term (case-insensitive substring), ranked by matched-term count, newest first on ties.
86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/ask/agent/memory.rb', line 86 def search(query, limit: 5) terms = query.to_s.downcase.gsub(/[^a-z0-9\s]/, " ").split(/\s+/).reject(&:empty?) return [] if terms.empty? scored = entries.filter_map do |entry| text = entry.content.downcase hits = terms.count { |term| text.include?(term) } [hits, entry] if hits.positive? end scored.sort_by { |hits, entry| [-hits, entry.created_at] }.first(limit).map(&:last) end |
#write(content, metadata: {}) ⇒ Entry
Save a fact. Writing an identical content again is a no-op (returns the existing entry).
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
# File 'lib/ask/agent/memory.rb', line 63 def write(content, metadata: {}) content = content.to_s raise ArgumentError, "content is required" if content.strip.empty? @mutex.synchronize do existing = entries.find { |e| e.content == content } return existing if existing entry = Entry.new(id: SecureRandom.uuid, content: content, metadata: , created_at: Time.now) @state.set(entry_key(entry.id), entry.to_h) @state.set(index_key, (load_index + [entry.id]).to_json) prune_oldest if @max_entries entry end end |