Memory¶
Reactive key-value store for sharing data between robots.
Class: RobotLab::Memory¶
Constants¶
RESERVED_KEYS¶
Reserved keys with special accessors and behavior.
Constructor¶
memory = Memory.new(
data: {},
results: [],
messages: [],
session_id: nil,
backend: :auto,
enable_cache: true,
network_name: nil
)
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
data |
Hash |
{} |
Initial runtime data |
results |
Array |
[] |
Pre-loaded robot results |
messages |
Array |
[] |
Pre-loaded conversation messages |
session_id |
String, nil |
nil |
Session identifier |
backend |
Symbol |
:auto |
Storage backend (:auto, :redis, :hash) |
enable_cache |
Boolean |
true |
Whether to enable semantic caching |
network_name |
String, nil |
nil |
Network this memory belongs to |
Factory Method¶
Methods¶
set¶
Store a value and notify subscribers asynchronously.
Parameters:
| Name | Type | Description |
|---|---|---|
key |
Symbol, String |
Storage key |
value |
Object |
Value to store |
get¶
memory.get(:key) # => value or nil
memory.get(:key, wait: true) # Block until available
memory.get(:key, wait: 30) # Block up to 30 seconds
memory.get(:a, :b, :c, wait: 60) # Multiple keys, returns Hash
Retrieve one or more values, optionally waiting until they exist.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
*keys |
Symbol, String |
— | One or more keys to retrieve; flattened and symbolized |
wait |
Boolean, Numeric |
false |
false: return immediately (nil if missing). true: block indefinitely. Numeric: block up to that many seconds |
Returns: the single value for one key, a Hash keyed by symbol for multiple keys.
Raises: RobotLab::AwaitTimeout — "Timeout waiting for :<key> after <N> seconds".
Blocking semantics
- On expiry
getraisesRobotLab::AwaitTimeout; it does not return nil. Rescue it if a missing value is acceptable. - With multiple keys the timeout is applied per missing key, not to the
call as a whole.
get(:a, :b, :c, wait: 60)can block for up to 180 seconds if all three are missing. wait: trueblocks with no deadline and can hang forever. Prefer a numeric timeout in production.- Waiting is implemented with a pipe (
Waiter, usingIO#wait_readable), so a blocked reader does not spin.
key?¶
Check if key exists. Aliases: has_key?, include?.
delete¶
Remove a key. Cannot delete reserved keys.
keys¶
All keys excluding the reserved ones.
all_keys¶
All keys including the reserved ones (:data, :results, :messages,
:session_id, :cache).
clear¶
Clear all non-reserved keys.
reset¶
Reset memory to its initial state: clears the backend, restores :data to {},
:results and :messages to [], :session_id to nil, and re-installs the
existing cache object. The StateProxy returned by data is discarded and
rebuilt on next access.
This resets the key-value store only. It has nothing to do with a robot's
chat history — use robot.clear_messages(keep_system: true) for that. The two
are independent.
subscribe¶
sub_id = memory.subscribe(:key1, :key2) do |change|
puts "#{change.key} changed: #{change.value}"
end
Subscribe to changes on one or more keys. Callback receives a MemoryChange object.
Returns a subscription ID for unsubscribe.
Raises: ArgumentError if no block is given.
Only set notifies subscribers. Writing a reserved key
(memory[:data] = ..., session_id=, append_result) bypasses notification
entirely, as does mutating memory.data.
Callbacks are dispatched through Async { }. Outside a running reactor the
callback runs synchronously on the writer's thread, so a slow subscriber blocks
the set that triggered it.
RobotLab::MemoryChange attributes:
| Attribute | Type | Description |
|---|---|---|
key |
Symbol |
The changed key |
value |
Object |
New value |
previous |
Object, nil |
Previous value |
writer |
String, nil |
memory.current_writer at the time of the write — set to the robot's name for the duration of each run |
network_name |
String, nil |
Network name |
timestamp |
Time |
When the change occurred |
correlation_id |
String, nil |
Optional tracing ID |
Predicates — note that each tests both sides, so all three are false when
previous and value are both nil, and all three are false for a nil→nil write:
| Predicate | Exact definition |
|---|---|
created? |
previous.nil? && !value.nil? |
updated? |
!previous.nil? && !value.nil? |
deleted? |
value.nil? && !previous.nil? |
Also available: to_h (.compacted, timestamp rendered as ISO-8601),
to_json, and MemoryChange.from_hash.
subscribe_pattern¶
sub_id = memory.subscribe_pattern("analysis:*") do |change|
puts "Analysis key #{change.key} updated"
end
Subscribe to keys matching a glob pattern (* and ? supported). Returns a
subscription ID. Raises: ArgumentError if no block is given.
unsubscribe¶
Remove a subscription by its ID (works for both subscribe and
subscribe_pattern). Returns true when something was removed.
unsubscribe_keys¶
Drop all key subscriptions for the named keys at once, without needing their IDs. Pattern subscriptions are unaffected.
subscribed?¶
Whether any subscriber — key-based or pattern-based — would be notified for key.
merge!¶
Merge multiple key-value pairs into memory. Each pair is assigned through []=,
so non-reserved keys go through the reactive set path and do notify subscribers.
Results and History¶
append_result¶
Push a RobotResult onto the accumulated :results array. Bypasses subscriber
notification.
set_results¶
Replace the whole :results array (used when loading from persistence).
results_from¶
Results from the given index onward — for incremental saves. Returns [] when
the index is past the end.
format_history¶
messages followed by every result flat-mapped through formatter. Pass a
Proc for formatter: to control how a RobotResult becomes messages; the
default formatter is used when omitted.
Backend¶
redis?¶
Whether this memory is backed by Redis rather than the in-process Hash. The
backend: :auto default tries Redis and falls back to a Hash; backend: :hash
forces the Hash.
network_name¶
The network this memory belongs to, set once at construction and read-only
thereafter. Network creates its shared memory as Memory.new(network_name: name);
a standalone robot's inherent memory has nil. It is copied onto every
MemoryChange so a subscriber can tell which network a write came from, and it is
preserved by clone/dup — but not by from_hash.
current_writer / current_writer=¶
The name attributed to writes, surfaced as MemoryChange#writer. Robot#run
sets this to the robot's name for the duration of the run and restores the
previous value in an ensure block, so nested and concurrent runs attribute
correctly.
Document Store¶
These four methods require the robot_lab-document_store extension gem. Without
it every one of them raises
RobotLab::DependencyError: document storage requires the robot_lab-document_store gem.
| Method | Returns | Description |
|---|---|---|
store_document(key, text) |
self |
Embed text and store it under key |
search_documents(query, limit: 5) |
Array<Hash> |
Hits sorted by score descending; each hash has :key, :text, :score |
document_keys |
Array<Symbol> |
Keys of all stored documents |
delete_document(key) |
self |
Remove a document |
memory.store_document(:readme, File.read("README.md"))
memory.search_documents("how to configure redis", limit: 3).each do |hit|
puts "#{hit[:key]} (#{hit[:score].round(3)})"
end
Documents live in a separate store, not in the key-value backend — they do not
appear in keys, all_keys, or to_h.
Reserved Key Accessors¶
data¶
memory.data # => StateProxy
memory.data[:user_id] # Hash access
memory.data.user_id # Method access
memory.data[:status] = "active"
Runtime data accessed through a StateProxy for method-style access.
Writes made through the proxy are not reactive — they bypass set, so they
wake no blocking readers and notify no subscribers.
results¶
Accumulated robot results (returns a copy).
messages¶
Conversation messages (returns a copy).
session_id¶
cache¶
Read-only after initialization — assigning it raises
ArgumentError: Cannot reassign cache - it is initialized automatically.
cache is nil when caching is disabled
The value stored is the RubyLLM::SemanticCache module, not an instance.
When constructed with enable_cache: false it is nil, so
memory.cache.fetch(...) raises NoMethodError. Guard on
memory.cache before use, or leave enable_cache at its true default.
Serialization¶
to_h¶
Keys: data (from the StateProxy), results (each via RobotResult#export),
messages (each via to_h), session_id, and custom (every non-reserved key).
cache is never serialized.
to_h is .compacted
Nil values are dropped, so session_id disappears entirely when unset:
RobotLab::Memory.new.to_h
# => { data: {}, results: [], messages: [], custom: {} } -- no :session_id
Consumers must tolerate the missing key. custom is always present (it is
{} rather than nil when there are no custom keys).
to_json¶
Serializes to_h.
from_hash¶
Reconstructs data, results, messages, and session_id, then re-applies
every entry from custom. A fresh cache is created; subscriptions, backend
choice, and network_name are not restored.
clone / dup¶
Deep copy of data plus copies of results, messages, session_id, and all
non-reserved keys. The enable_cache setting and network_name are preserved.
Subscriptions are not copied — the clone starts with none. Copying custom
keys uses the internal non-reactive setter, so no notifications fire.
Examples¶
Basic Usage¶
robot.memory.set(:user_name, "Alice")
robot.memory.set(:order_count, 5)
name = robot.memory.get(:user_name) # => "Alice"
count = robot.memory.get(:order_count) # => 5
Bracket Access¶
Storing Objects¶
robot.memory.set(:user, {
id: 123,
name: "Alice",
plan: "pro"
})
user = robot.memory.get(:user)
user[:plan] # => "pro"
Blocking Reads (Network Parallel Execution)¶
# In robot A (writer)
network.memory.set(:sentiment, { score: 0.8, confidence: 0.95 })
# In robot B (reader, may run concurrently)
result = network.memory.get(:sentiment, wait: true) # Block indefinitely
result = network.memory.get(:sentiment, wait: 30) # Block up to 30s, then raise
# Multiple keys — the timeout applies PER MISSING KEY, so this can block 180s
results = network.memory.get(:sentiment, :entities, :keywords, wait: 60)
# => { sentiment: {...}, entities: [...], keywords: [...] }
# Treat a timeout as "not available"
sentiment = begin
network.memory.get(:sentiment, wait: 30)
rescue RobotLab::AwaitTimeout
nil
end
Reactive Subscriptions¶
# Subscribe to a key
memory.subscribe(:raw_data) do |change|
enriched = enrich(change.value)
memory.set(:enriched, enriched)
end
# Subscribe with pattern
memory.subscribe_pattern("user:*") do |change|
puts "User key #{change.key} updated by #{change.writer}"
end
Cross-Robot Communication via Network Memory¶
# In classifier robot
network.memory.set(:intent, "billing")
network.memory.set(:entities, ["order", "refund"])
# In handler robot
intent = network.memory.get(:intent)
entities = network.memory.get(:entities)
Data Proxy¶
memory = RobotLab.create_memory(
data: { user: { name: "Alice", plan: "pro" } }
)
memory.data[:user][:name] # => "Alice"
memory.data.to_h # => { user: { name: "Alice", plan: "pro" } }
Serialization¶
# Save memory
json = memory.to_json
File.write("memory.json", json)
# Restore memory
data = JSON.parse(File.read("memory.json"))
memory = Memory.from_hash(data)
See Also¶
- Memory Guide
- StateProxy — the wrapper returned by
memory.data - State Management Architecture