Class: Pikuri::Memory::Extension
- Inherits:
-
Object
- Object
- Pikuri::Memory::Extension
- Includes:
- Agent::Extension
- Defined in:
- lib/pikuri/memory/extension.rb
Overview
The host-facing API: wire durable cross-conversation memory onto a Agent via c.add_extension inside the Agent.new block — same opt-in shape as pikuri-tasks / pikuri-vectordb.
Usage
client = Pikuri::Memory::Mem0Client.new(endpoint: 'http://localhost:8888')
Pikuri::Agent.new(transport: ..., system_prompt: ...) do |c|
c.add_extension Pikuri::Memory::Extension.new(
client: client, user_id: 'martin'
)
end
What it wires (the three retrieval tiers)
-
configure registers the
recalltool (Recall) and, whenresident_persona:is on, appends a small always-in-prompt persona summary read from the store (tier 1 + tier 3). -
on_user_message (the per-turn hook) does tier 2 — the automatic prefetch — and the asynchronous capture: it enqueues the user’s turn for off-path extraction, then returns a small <memory-context> slice for the Agent to inject as a
:systemmessage after the user turn. -
bind starts the capture worker and arms its bounded flush on agent close.
Read sync, write async
Prefetch runs on the interaction path because a vector search is milliseconds; capture runs off it through Recorder because extraction is a ~3s LLM call. Recalled context is :system-role (provenance-tagged, excluded from the next extraction pass), and only the user’s own words are captured — the two halves of the feedback-loop defense (DESIGN.md §“Retrieval”).
Safety scope
Automatic capture + recall are safe only on a no-untrusted-ingest, no-egress agent (the @private configuration). See the Pikuri::Memory namespace header.
Sub-agents
Sub-agents do not inherit extensions, so a delegated persona’s turns are never prefetched or captured by the parent’s memory —consistent with the no-inherit rule.
Constant Summary collapse
- LOGGER =
Pikuri.logger_for('Memory::Extension')
- DEFAULT_PREFETCH_K =
Returns default prefetch slice size — small and high-precision, since junk recall degrades behavior and the
recalltool means a small slice is a pointer, not a loss (DESIGN.md §“Automatic ≠ always-inject”). 5- DEFAULT_RESIDENT_LIMIT =
Returns default cap on the resident-persona summary —a few facts, not the whole store. Curated synthesis is a follow-up; v1 takes the first Mem0Client#get_all rows.
20
Instance Attribute Summary collapse
-
#recorder ⇒ Recorder
readonly
The capture queue, exposed for tests and for a host that wants to flush it explicitly.
Instance Method Summary collapse
-
#bind(agent) ⇒ void
Start the capture worker and arm its bounded flush on agent close.
-
#configure(c) ⇒ void
Register the
recalltool and (optionally) append the resident persona summary. - #initialize(client:, user_id:, prefetch_k: DEFAULT_PREFETCH_K, threshold: nil, infer: true, extraction_prompt: nil, resident_persona: true, flush_timeout: Recorder::DEFAULT_FLUSH_TIMEOUT, resident_limit: DEFAULT_RESIDENT_LIMIT) ⇒ Extension constructor
-
#on_user_message(_agent, content) ⇒ String?
Per-turn hook.
Constructor Details
#initialize(client:, user_id:, prefetch_k: DEFAULT_PREFETCH_K, threshold: nil, infer: true, extraction_prompt: nil, resident_persona: true, flush_timeout: Recorder::DEFAULT_FLUSH_TIMEOUT, resident_limit: DEFAULT_RESIDENT_LIMIT) ⇒ Extension
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/pikuri/memory/extension.rb', line 101 def initialize(client:, user_id:, prefetch_k: DEFAULT_PREFETCH_K, threshold: nil, infer: true, extraction_prompt: nil, resident_persona: true, flush_timeout: Recorder::DEFAULT_FLUSH_TIMEOUT, resident_limit: DEFAULT_RESIDENT_LIMIT) raise ArgumentError, 'user_id must be non-empty' if user_id.nil? || user_id.to_s.empty? @client = client @user_id = user_id @prefetch_k = prefetch_k @threshold = threshold @resident_persona = resident_persona @resident_limit = resident_limit # nil => mem0's built-in extraction (reliable). The bundled curated # prompt is opt-in (a WIP that under-extracts on small models) — see # the +extraction_prompt+ param doc. @extraction_prompt = extraction_prompt @recorder = Recorder.new( client: client, user_id: user_id, infer: infer, prompt: @extraction_prompt, flush_timeout: flush_timeout ) end |
Instance Attribute Details
#recorder ⇒ Recorder (readonly)
Returns the capture queue, exposed for tests and for a host that wants to flush it explicitly.
126 127 128 |
# File 'lib/pikuri/memory/extension.rb', line 126 def recorder @recorder end |
Instance Method Details
#bind(agent) ⇒ void
This method returns an undefined value.
Start the capture worker and arm its bounded flush on agent close. Keyed to this specific agent via Agent#on_close (not Configurator#on_close) so the lifetime tracks the live agent.
157 158 159 160 161 |
# File 'lib/pikuri/memory/extension.rb', line 157 def bind(agent) @recorder.start agent.on_close { @recorder.close } nil end |
#configure(c) ⇒ void
This method returns an undefined value.
Register the recall tool and (optionally) append the resident persona summary. Raises if recall was pre-registered — the extension is its sole owner and a duplicate would bind to a different client / namespace.
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/pikuri/memory/extension.rb', line 135 def configure(c) if c.tools.any? { |t| t.name == 'recall' } raise 'recall cannot be pre-registered (in tools: or via c.add_tool) when adding ' \ 'Pikuri::Memory::Extension — the extension owns the recall tool so it shares ' \ 'the same mem0 client / user_id.' end c.add_tool Recall.new(client: @client, user_id: @user_id) return unless @resident_persona snippet = resident_persona_snippet c.append_system_prompt(snippet) if snippet nil end |
#on_user_message(_agent, content) ⇒ String?
Per-turn hook. Enqueues the user’s words for asynchronous capture, then returns the automatic prefetch slice (or nil to inject nothing this turn). Capture happens regardless of whether prefetch finds anything.
172 173 174 175 |
# File 'lib/pikuri/memory/extension.rb', line 172 def (_agent, content) @recorder.enqueue(content) prefetch(content) end |