Memory Management¶
Memory in RobotLab is a reactive key-value store that provides persistent storage for runtime data, conversation history, and arbitrary user-defined values. A single unified Memory class serves both standalone robot usage and shared network execution.
Memory Structure¶
The Memory class holds:
memory = RobotLab.create_memory(data: { user_id: "123" })
memory.data # StateProxy - custom key-value data with method-style access
memory.results # Array<RobotResult> - execution history
memory.messages # Array<Message> - conversation history
memory.session_id # String - optional persistence identifier
memory.cache # RubyLLM::SemanticCache - semantic caching module
Standalone Robot Memory¶
Every robot has its own inherent memory instance, accessible via robot.memory:
robot = RobotLab.build(
name: "assistant",
system_prompt: "You are helpful."
)
# Access the robot's memory
robot.memory[:user_name] = "Alice"
robot.memory[:user_name] #=> "Alice"
# Run the robot
result = robot.run("Hello!")
# Memory persists between runs on the same robot instance
robot.memory[:preference] = "dark_mode"
result2 = robot.run("What are my preferences?")
# Reset memory to initial state
robot.reset_memory
reset_memory resets only the key-value store. It does not touch the robot's chat history — that is robot.clear_messages(keep_system: true). The two are independent.
Network Shared Memory¶
When robots execute within a network, they share the network's memory instead of using their own inherent memory. This enables inter-robot communication.
classifier = RobotLab.build(name: "classifier", system_prompt: "Classify requests.")
handler = RobotLab.build(name: "handler", system_prompt: "Handle requests.")
network = RobotLab.create_network(name: "support") do
task :classifier, classifier, depends_on: :none
task :handler, handler, depends_on: [:classifier]
end
# The network has its own shared memory
network.memory[:customer_tier] = "premium"
# All robots in the network read/write from network.memory during execution
result = network.run(message: "I need help with billing")
# Reset network memory between runs if needed
network.reset_memory
The memory resolution logic is:
- If
network_memoryis provided at runtime, use that - If the robot is in a network, use the network's shared memory
- Otherwise, use the robot's own inherent memory (
robot.memory)
Creating Memory¶
Basic Creation¶
With Initial Data¶
memory = RobotLab.create_memory(
data: {
user_id: "user_123",
order_id: "ord_456",
priority: "high"
}
)
With Caching Disabled¶
Reserved Keys¶
Memory has five reserved keys with special behavior and dedicated accessors:
| Key | Type | Description |
|---|---|---|
:data |
StateProxy |
Runtime data with method-style access |
:results |
Array<RobotResult> |
Accumulated robot execution results |
:messages |
Array<Message> |
Conversation history |
:session_id |
String |
Conversation session identifier |
:cache |
RubyLLM::SemanticCache |
Semantic cache module (read-only after init) |
Reserved keys are accessed through dedicated methods and are excluded from memory.keys:
memory.data[:category] = "billing"
memory.data.category #=> "billing" (method-style via StateProxy)
memory.results #=> []
memory.session_id #=> nil
memory.cache #=> RubyLLM::SemanticCache (the module itself), or nil when enable_cache: false
StateProxy¶
The data attribute is a StateProxy that provides convenient hash-style and method-style access:
memory.data[:user_id] # Hash-style access
memory.data[:user_id] = "456" # Assignment
memory.data.user_id # Method-style access
memory.data.user_id = "456" # Method-style assignment
memory.data.key?(:user_id) # Check existence
memory.data.keys # Get all keys
memory.data.to_h # Convert to plain hash
Reactive Features¶
Memory supports pub/sub semantics where robots can subscribe to key changes and optionally block until values become available.
Setting Values¶
Use memory.set(key, value) to write a value, wake any waiters, and notify subscribers. Subscriber callbacks are dispatched through Async { }: inside a running reactor they run as fibers, but outside one they run synchronously on the writer's thread.
The []= operator also triggers reactive notifications for non-reserved keys:
Blocking Reads¶
Use memory.get(key, wait:) to block until a value becomes available. This is useful for concurrent pipeline execution where one robot needs to wait for another's output:
# Immediate read (returns nil if missing)
memory.get(:sentiment)
# Block indefinitely until value exists
memory.get(:sentiment, wait: true)
# Block up to 30 seconds, then raise RobotLab::AwaitTimeout
memory.get(:sentiment, wait: 30)
# Wait for multiple keys at once
results = memory.get(:sentiment, :entities, :keywords, wait: 60)
#=> { sentiment: {...}, entities: [...], keywords: [...] }
Two things to be careful with:
- On expiry a blocking read raises
RobotLab::AwaitTimeout; it does not returnnil. 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_multiplewaits on the missing keys one at a time in order, so the call raises as soon as the first one expires — it does not spend the full 60 s on each. The 180 s worst case is only reached when the earlier keys arrive just before their own deadlines.
Subscriptions¶
Subscribe to key changes with async callbacks. The callback receives a MemoryChange object:
memory.subscribe(:raw_data) do |change|
puts "#{change.key} changed from #{change.previous} to #{change.value}"
puts "Written by: #{change.writer} at #{change.timestamp}"
end
# Subscribe to multiple keys
memory.subscribe(:sentiment, :entities) do |change|
update_dashboard(change.key, change.value)
end
# Pattern-based subscriptions (glob-style matching)
memory.subscribe_pattern("analysis:*") do |change|
puts "Analysis key #{change.key} updated"
end
# Unsubscribe
sub_id = memory.subscribe(:status) { |c| puts c.value }
memory.unsubscribe(sub_id)
# Check if key has subscribers
memory.subscribed?(:status) #=> true/false
MemoryChange¶
The MemoryChange object provides context about what changed:
change.key #=> :sentiment
change.value #=> { score: 0.8 }
change.previous #=> nil (or previous value)
change.writer #=> "classifier" (robot name)
change.network_name #=> "support_pipeline"
change.timestamp #=> Time
change.created? #=> true (new key, no previous value)
change.updated? #=> false
change.deleted? #=> false
Memory Lifecycle¶
Results¶
Results track the history of robot executions:
# Append a result
memory.append_result(robot_result)
# Get all results (returns a copy)
memory.results
# Get results from a specific index (for incremental persistence)
memory.results_from(5)
Each RobotResult contains:
result.robot_name # Which robot produced this
result.output # [TextMessage] synthesized from the final response text
result.tool_calls # Array<ToolResultMessage> - effectively always empty (see Message Flow)
result.stop_reason # always nil for a robot.run result (see Message Flow)
result.last_text_content # Convenience: last text content string
result.input_tokens # Prompt tokens for the run
result.output_tokens # Completion tokens for the run
result.created_at # When it was created
Format History¶
The format_history method prepares messages for LLM consumption:
Merge¶
Merge additional values into memory:
Key Management¶
memory.key?(:user_id) # Check existence
memory.keys # Get all non-reserved keys
memory.all_keys # Get all keys including reserved
memory.delete(:temp_data) # Delete a specific key
memory.clear # Clear all non-reserved keys
memory.reset # Reset to initial state (preserves cache)
Cloning¶
Create independent copies of memory for isolated execution. Subscriptions are not cloned:
original = RobotLab.create_memory(data: { count: 1 })
cloned = original.clone
cloned[:count] = 2
original[:count] #=> still 1
Serialization¶
Convert memory to and from hash for persistence:
# To hash
hash = memory.to_h
#=> {
# data: { ... },
# results: [...],
# messages: [...],
# session_id: "abc123",
# custom: { my_key: "value" }
# }
# To JSON
json = memory.to_json
# From hash
memory = Memory.from_hash(hash)
to_h is compacted, so nil-valued reserved keys (a missing session_id, for instance) are omitted entirely rather than serialized as nil.
Semantic Cache¶
Memory includes a semantic cache via RubyLLM::SemanticCache that reduces costs and latency by returning cached responses for semantically equivalent queries:
# Using the cache with fetch
response = memory.cache.fetch("What is Ruby?") do
RubyLLM.chat.ask("What is Ruby?")
end
# Wrapping a chat instance
chat = memory.cache.wrap(RubyLLM.chat(model: "gpt-4o"))
chat.ask("What is Ruby?") # Cached on semantic similarity
Caching can be disabled per-memory or per-robot:
memory = RobotLab.create_memory(enable_cache: false)
robot = RobotLab.build(name: "bot", system_prompt: "...", enable_cache: false)
Backend Options¶
Memory defaults to a Hash-based backend but can use Redis for distributed scenarios:
# Try Redis, fall back to Hash
memory = Memory.new(backend: :auto)
# Hash backend — the only value that is honoured literally
memory = Memory.new(backend: :hash)
# Same behaviour as :auto — try Redis, fall back to Hash
memory = Memory.new(backend: :redis)
# Check what you actually got
memory.redis? #=> true/false
backend: :redis does not force Redis
select_backend special-cases only :hash; every other value (:redis,
:auto, anything else) takes the same branch —
create_redis_backend || create_hash_backend. create_redis_backend
returns nil when redis is unavailable and rescues StandardError to
nil on a connection failure, so an unreachable Redis silently downgrades
to a Hash with no exception and no warning. Always check memory.redis? if
the distinction matters.
Redis is configured via RobotLab.config.redis or the REDIS_URL environment variable.
Best Practices¶
1. Use Memory for Cross-Robot Data¶
# In a network, robots share memory automatically.
# Robot A writes:
memory.set(:classification, "billing")
# Robot B reads:
category = memory.get(:classification)
2. Use Blocking Reads for Concurrent Pipelines¶
# When robots run in parallel, use blocking reads
# to synchronize on shared data:
results = memory.get(:sentiment, :entities, wait: 60)
3. Keep Data Minimal¶
# Store references instead of large objects
memory[:response_id] = response.id # Preferred
# memory[:huge_response] = api_response # Avoid
4. Reset Between Independent Runs¶
Next Steps¶
- Network Orchestration - How networks share memory
- Message Flow - How messages are processed