robot_lab 0.0.1 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/deploy-github-pages.yml +9 -9
- data/.irbrc +6 -0
- data/CHANGELOG.md +140 -0
- data/README.md +263 -48
- data/Rakefile +71 -1
- data/docs/api/core/index.md +53 -46
- data/docs/api/core/memory.md +200 -154
- data/docs/api/core/network.md +13 -3
- data/docs/api/core/robot.md +490 -130
- data/docs/api/core/state.md +55 -73
- data/docs/api/core/tool.md +205 -209
- data/docs/api/index.md +7 -28
- data/docs/api/mcp/client.md +119 -48
- data/docs/api/mcp/index.md +75 -60
- data/docs/api/mcp/server.md +120 -136
- data/docs/api/mcp/transports.md +172 -184
- data/docs/api/messages/index.md +35 -20
- data/docs/api/messages/text-message.md +67 -21
- data/docs/api/messages/tool-call-message.md +80 -41
- data/docs/api/messages/tool-result-message.md +119 -50
- data/docs/api/messages/user-message.md +48 -24
- data/docs/api/streaming/context.md +157 -74
- data/docs/api/streaming/events.md +114 -166
- data/docs/api/streaming/index.md +74 -72
- data/docs/architecture/core-concepts.md +360 -116
- data/docs/architecture/index.md +97 -59
- data/docs/architecture/message-flow.md +138 -129
- data/docs/architecture/network-orchestration.md +197 -50
- data/docs/architecture/robot-execution.md +199 -146
- data/docs/architecture/state-management.md +255 -187
- data/docs/concepts.md +311 -49
- data/docs/examples/basic-chat.md +89 -77
- data/docs/examples/index.md +222 -47
- data/docs/examples/mcp-server.md +207 -203
- data/docs/examples/multi-robot-network.md +129 -35
- data/docs/examples/rails-application.md +159 -160
- data/docs/examples/tool-usage.md +295 -204
- data/docs/getting-started/configuration.md +347 -154
- data/docs/getting-started/index.md +1 -1
- data/docs/getting-started/installation.md +22 -13
- data/docs/getting-started/quick-start.md +166 -121
- data/docs/guides/building-robots.md +418 -212
- data/docs/guides/creating-networks.md +143 -24
- data/docs/guides/index.md +0 -5
- data/docs/guides/mcp-integration.md +152 -113
- data/docs/guides/memory.md +220 -164
- data/docs/guides/rails-integration.md +244 -162
- data/docs/guides/streaming.md +137 -187
- data/docs/guides/using-tools.md +259 -212
- data/docs/index.md +46 -41
- data/examples/01_simple_robot.rb +6 -9
- data/examples/02_tools.rb +6 -9
- data/examples/03_network.rb +19 -17
- data/examples/04_mcp.rb +5 -8
- data/examples/05_streaming.rb +5 -8
- data/examples/06_prompt_templates.rb +42 -37
- data/examples/07_network_memory.rb +13 -14
- data/examples/08_llm_config.rb +169 -0
- data/examples/09_chaining.rb +262 -0
- data/examples/10_memory.rb +331 -0
- data/examples/11_network_introspection.rb +253 -0
- data/examples/12_message_bus.rb +74 -0
- data/examples/13_spawn.rb +90 -0
- data/examples/14_rusty_circuit/comic.rb +143 -0
- data/examples/14_rusty_circuit/display.rb +203 -0
- data/examples/14_rusty_circuit/heckler.rb +63 -0
- data/examples/14_rusty_circuit/open_mic.rb +123 -0
- data/examples/14_rusty_circuit/prompts/open_mic_comic.md +20 -0
- data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +23 -0
- data/examples/14_rusty_circuit/prompts/open_mic_scout.md +20 -0
- data/examples/14_rusty_circuit/scout.rb +156 -0
- data/examples/14_rusty_circuit/scout_notes.md +89 -0
- data/examples/14_rusty_circuit/show.log +234 -0
- data/examples/15_memory_network_and_bus/editor_in_chief.rb +24 -0
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +206 -0
- data/examples/15_memory_network_and_bus/linux_writer.rb +80 -0
- data/examples/15_memory_network_and_bus/os_editor.rb +46 -0
- data/examples/15_memory_network_and_bus/os_writer.rb +46 -0
- data/examples/15_memory_network_and_bus/output/combined_article.md +13 -0
- data/examples/15_memory_network_and_bus/output/final_article.md +15 -0
- data/examples/15_memory_network_and_bus/output/linux_draft.md +5 -0
- data/examples/15_memory_network_and_bus/output/mac_draft.md +7 -0
- data/examples/15_memory_network_and_bus/output/memory.json +13 -0
- data/examples/15_memory_network_and_bus/output/revision_1.md +19 -0
- data/examples/15_memory_network_and_bus/output/revision_2.md +15 -0
- data/examples/15_memory_network_and_bus/output/windows_draft.md +7 -0
- data/examples/15_memory_network_and_bus/prompts/os_advocate.md +13 -0
- data/examples/15_memory_network_and_bus/prompts/os_chief.md +13 -0
- data/examples/15_memory_network_and_bus/prompts/os_editor.md +13 -0
- data/examples/16_writers_room/display.rb +158 -0
- data/examples/16_writers_room/output/.gitignore +2 -0
- data/examples/16_writers_room/output/opus_001.md +263 -0
- data/examples/16_writers_room/output/opus_001_notes.log +470 -0
- data/examples/16_writers_room/prompts/writer.md +37 -0
- data/examples/16_writers_room/room.rb +150 -0
- data/examples/16_writers_room/tools.rb +162 -0
- data/examples/16_writers_room/writer.rb +121 -0
- data/examples/16_writers_room/writers_room.rb +162 -0
- data/examples/README.md +197 -0
- data/examples/prompts/{assistant/system.txt.erb → assistant.md} +3 -0
- data/examples/prompts/{billing/system.txt.erb → billing.md} +3 -0
- data/examples/prompts/{classifier/system.txt.erb → classifier.md} +3 -0
- data/examples/prompts/comedian.md +6 -0
- data/examples/prompts/comedy_critic.md +10 -0
- data/examples/prompts/configurable.md +9 -0
- data/examples/prompts/dispatcher.md +12 -0
- data/examples/prompts/{entity_extractor/system.txt.erb → entity_extractor.md} +3 -0
- data/examples/prompts/{escalation/system.txt.erb → escalation.md} +7 -0
- data/examples/prompts/frontmatter_mcp_test.md +9 -0
- data/examples/prompts/frontmatter_named_test.md +5 -0
- data/examples/prompts/frontmatter_tools_test.md +6 -0
- data/examples/prompts/{general/system.txt.erb → general.md} +3 -0
- data/examples/prompts/{github_assistant/system.txt.erb → github_assistant.md} +8 -0
- data/examples/prompts/{helper/system.txt.erb → helper.md} +3 -0
- data/examples/prompts/{keyword_extractor/system.txt.erb → keyword_extractor.md} +3 -0
- data/examples/prompts/llm_config_demo.md +20 -0
- data/examples/prompts/{order_support/system.txt.erb → order_support.md} +8 -0
- data/examples/prompts/os_advocate.md +13 -0
- data/examples/prompts/os_chief.md +13 -0
- data/examples/prompts/os_editor.md +13 -0
- data/examples/prompts/{product_support/system.txt.erb → product_support.md} +7 -0
- data/examples/prompts/{sentiment_analyzer/system.txt.erb → sentiment_analyzer.md} +3 -0
- data/examples/prompts/{synthesizer/system.txt.erb → synthesizer.md} +3 -0
- data/examples/prompts/{technical/system.txt.erb → technical.md} +3 -0
- data/examples/prompts/{triage/system.txt.erb → triage.md} +6 -0
- data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -13
- data/lib/robot_lab/ask_user.rb +75 -0
- data/lib/robot_lab/config/defaults.yml +121 -0
- data/lib/robot_lab/config.rb +183 -0
- data/lib/robot_lab/error.rb +6 -0
- data/lib/robot_lab/mcp/client.rb +1 -1
- data/lib/robot_lab/memory.rb +10 -34
- data/lib/robot_lab/network.rb +13 -20
- data/lib/robot_lab/robot/bus_messaging.rb +239 -0
- data/lib/robot_lab/robot/mcp_management.rb +88 -0
- data/lib/robot_lab/robot/template_rendering.rb +130 -0
- data/lib/robot_lab/robot.rb +240 -330
- data/lib/robot_lab/robot_message.rb +44 -0
- data/lib/robot_lab/robot_result.rb +1 -0
- data/lib/robot_lab/run_config.rb +184 -0
- data/lib/robot_lab/state_proxy.rb +2 -12
- data/lib/robot_lab/streaming/context.rb +1 -1
- data/lib/robot_lab/task.rb +8 -1
- data/lib/robot_lab/tool.rb +108 -172
- data/lib/robot_lab/tool_config.rb +1 -1
- data/lib/robot_lab/tool_manifest.rb +2 -18
- data/lib/robot_lab/utils.rb +39 -0
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +89 -57
- data/mkdocs.yml +0 -11
- metadata +121 -135
- data/docs/api/adapters/anthropic.md +0 -121
- data/docs/api/adapters/gemini.md +0 -133
- data/docs/api/adapters/index.md +0 -104
- data/docs/api/adapters/openai.md +0 -134
- data/docs/api/history/active-record-adapter.md +0 -195
- data/docs/api/history/config.md +0 -191
- data/docs/api/history/index.md +0 -132
- data/docs/api/history/thread-manager.md +0 -144
- data/docs/guides/history.md +0 -359
- data/examples/prompts/assistant/user.txt.erb +0 -1
- data/examples/prompts/billing/user.txt.erb +0 -1
- data/examples/prompts/classifier/user.txt.erb +0 -1
- data/examples/prompts/entity_extractor/user.txt.erb +0 -3
- data/examples/prompts/escalation/user.txt.erb +0 -34
- data/examples/prompts/general/user.txt.erb +0 -1
- data/examples/prompts/github_assistant/user.txt.erb +0 -1
- data/examples/prompts/helper/user.txt.erb +0 -1
- data/examples/prompts/keyword_extractor/user.txt.erb +0 -3
- data/examples/prompts/order_support/user.txt.erb +0 -22
- data/examples/prompts/product_support/user.txt.erb +0 -32
- data/examples/prompts/sentiment_analyzer/user.txt.erb +0 -3
- data/examples/prompts/synthesizer/user.txt.erb +0 -15
- data/examples/prompts/technical/user.txt.erb +0 -1
- data/examples/prompts/triage/user.txt.erb +0 -17
- data/lib/robot_lab/adapters/anthropic.rb +0 -163
- data/lib/robot_lab/adapters/base.rb +0 -85
- data/lib/robot_lab/adapters/gemini.rb +0 -193
- data/lib/robot_lab/adapters/openai.rb +0 -159
- data/lib/robot_lab/adapters/registry.rb +0 -81
- data/lib/robot_lab/configuration.rb +0 -143
- data/lib/robot_lab/errors.rb +0 -70
- data/lib/robot_lab/history/active_record_adapter.rb +0 -146
- data/lib/robot_lab/history/config.rb +0 -115
- data/lib/robot_lab/history/thread_manager.rb +0 -93
- data/lib/robot_lab/robotic_model.rb +0 -324
|
@@ -1,37 +1,87 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Memory Management
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Memory in RobotLab is a reactive key-value store that provides persistent storage for runtime data, conversation history, and arbitrary user-defined values. It replaces the old `State` class with a unified system that supports both standalone robot usage and shared network execution.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Memory Structure
|
|
6
6
|
|
|
7
|
-
The `
|
|
7
|
+
The `Memory` class holds:
|
|
8
8
|
|
|
9
9
|
```ruby
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
memory = RobotLab.create_memory(data: { user_id: "123" })
|
|
11
|
+
|
|
12
|
+
memory.data # StateProxy - custom key-value data with method-style access
|
|
13
|
+
memory.results # Array<RobotResult> - execution history
|
|
14
|
+
memory.messages # Array<Message> - conversation history
|
|
15
|
+
memory.session_id # String - optional persistence identifier
|
|
16
|
+
memory.cache # RubyLLM::SemanticCache - semantic caching module
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Standalone Robot Memory
|
|
20
|
+
|
|
21
|
+
Every robot has its own inherent memory instance, accessible via `robot.memory`:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
robot = RobotLab.build(
|
|
25
|
+
name: "assistant",
|
|
26
|
+
system_prompt: "You are helpful."
|
|
13
27
|
)
|
|
14
28
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
29
|
+
# Access the robot's memory
|
|
30
|
+
robot.memory[:user_name] = "Alice"
|
|
31
|
+
robot.memory[:user_name] #=> "Alice"
|
|
32
|
+
|
|
33
|
+
# Run the robot
|
|
34
|
+
result = robot.run("Hello!")
|
|
35
|
+
|
|
36
|
+
# Memory persists between runs on the same robot instance
|
|
37
|
+
robot.memory[:preference] = "dark_mode"
|
|
38
|
+
result2 = robot.run("What are my preferences?")
|
|
39
|
+
|
|
40
|
+
# Reset memory to initial state
|
|
41
|
+
robot.reset_memory
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Network Shared Memory
|
|
45
|
+
|
|
46
|
+
When robots execute within a network, they share the network's memory instead of using their own inherent memory. This enables inter-robot communication.
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
classifier = RobotLab.build(name: "classifier", system_prompt: "Classify requests.")
|
|
50
|
+
handler = RobotLab.build(name: "handler", system_prompt: "Handle requests.")
|
|
51
|
+
|
|
52
|
+
network = RobotLab.create_network(name: "support") do
|
|
53
|
+
task :classifier, classifier, depends_on: :none
|
|
54
|
+
task :handler, handler, depends_on: [:classifier]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The network has its own shared memory
|
|
58
|
+
network.memory[:customer_tier] = "premium"
|
|
59
|
+
|
|
60
|
+
# All robots in the network read/write from network.memory during execution
|
|
61
|
+
result = network.run(message: "I need help with billing")
|
|
62
|
+
|
|
63
|
+
# Reset network memory between runs if needed
|
|
64
|
+
network.reset_memory
|
|
20
65
|
```
|
|
21
66
|
|
|
22
|
-
|
|
67
|
+
The memory resolution logic is:
|
|
68
|
+
|
|
69
|
+
1. If `network_memory` is provided at runtime, use that
|
|
70
|
+
2. If the robot is in a network, use the network's shared memory
|
|
71
|
+
3. Otherwise, use the robot's own inherent memory (`robot.memory`)
|
|
72
|
+
|
|
73
|
+
## Creating Memory
|
|
23
74
|
|
|
24
75
|
### Basic Creation
|
|
25
76
|
|
|
26
77
|
```ruby
|
|
27
|
-
|
|
78
|
+
memory = RobotLab.create_memory
|
|
28
79
|
```
|
|
29
80
|
|
|
30
|
-
### With
|
|
81
|
+
### With Initial Data
|
|
31
82
|
|
|
32
83
|
```ruby
|
|
33
|
-
|
|
34
|
-
message: "Process my order",
|
|
84
|
+
memory = RobotLab.create_memory(
|
|
35
85
|
data: {
|
|
36
86
|
user_id: "user_123",
|
|
37
87
|
order_id: "ord_456",
|
|
@@ -40,284 +90,302 @@ state = RobotLab.create_state(
|
|
|
40
90
|
)
|
|
41
91
|
```
|
|
42
92
|
|
|
43
|
-
###
|
|
93
|
+
### With Caching Disabled
|
|
44
94
|
|
|
45
95
|
```ruby
|
|
46
|
-
|
|
47
|
-
message: "Continue our conversation",
|
|
48
|
-
results: previous_results,
|
|
49
|
-
thread_id: "thread_abc"
|
|
50
|
-
)
|
|
96
|
+
memory = RobotLab.create_memory(data: {}, enable_cache: false)
|
|
51
97
|
```
|
|
52
98
|
|
|
53
|
-
##
|
|
54
|
-
|
|
55
|
-
The `data` attribute is a `StateProxy` that provides convenient access:
|
|
56
|
-
|
|
57
|
-
```ruby
|
|
58
|
-
state.data[:user_id] # Hash-style access
|
|
59
|
-
state.data[:user_id] = "456" # Assignment
|
|
60
|
-
|
|
61
|
-
state.data.user_id # Method-style access
|
|
62
|
-
state.data.user_id = "456" # Method-style assignment
|
|
99
|
+
## Reserved Keys
|
|
63
100
|
|
|
64
|
-
|
|
65
|
-
state.data.keys # Get all keys
|
|
66
|
-
state.data.to_h # Convert to plain hash
|
|
67
|
-
```
|
|
101
|
+
Memory has five reserved keys with special behavior and dedicated accessors:
|
|
68
102
|
|
|
69
|
-
|
|
103
|
+
| Key | Type | Description |
|
|
104
|
+
|-----|------|-------------|
|
|
105
|
+
| `:data` | `StateProxy` | Runtime data with method-style access |
|
|
106
|
+
| `:results` | `Array<RobotResult>` | Accumulated robot execution results |
|
|
107
|
+
| `:messages` | `Array<Message>` | Conversation history |
|
|
108
|
+
| `:session_id` | `String` | Conversation session identifier |
|
|
109
|
+
| `:cache` | `RubyLLM::SemanticCache` | Semantic cache module (read-only after init) |
|
|
70
110
|
|
|
71
|
-
|
|
111
|
+
Reserved keys are accessed through dedicated methods and are excluded from `memory.keys`:
|
|
72
112
|
|
|
73
113
|
```ruby
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
on_change: ->(key, old_val, new_val) {
|
|
77
|
-
puts "#{key}: #{old_val} -> #{new_val}"
|
|
78
|
-
}
|
|
79
|
-
)
|
|
114
|
+
memory.data[:category] = "billing"
|
|
115
|
+
memory.data.category #=> "billing" (method-style via StateProxy)
|
|
80
116
|
|
|
81
|
-
|
|
117
|
+
memory.results #=> []
|
|
118
|
+
memory.session_id #=> nil
|
|
119
|
+
memory.cache #=> RubyLLM::SemanticCache
|
|
82
120
|
```
|
|
83
121
|
|
|
84
|
-
##
|
|
122
|
+
## StateProxy
|
|
85
123
|
|
|
86
|
-
|
|
124
|
+
The `data` attribute is a `StateProxy` that provides convenient hash-style and method-style access:
|
|
87
125
|
|
|
88
126
|
```ruby
|
|
89
|
-
#
|
|
90
|
-
|
|
91
|
-
state.memory.remember("preferences", { theme: "dark" })
|
|
127
|
+
memory.data[:user_id] # Hash-style access
|
|
128
|
+
memory.data[:user_id] = "456" # Assignment
|
|
92
129
|
|
|
93
|
-
#
|
|
94
|
-
|
|
130
|
+
memory.data.user_id # Method-style access
|
|
131
|
+
memory.data.user_id = "456" # Method-style assignment
|
|
95
132
|
|
|
96
|
-
# Check existence
|
|
97
|
-
|
|
133
|
+
memory.data.key?(:user_id) # Check existence
|
|
134
|
+
memory.data.keys # Get all keys
|
|
135
|
+
memory.data.to_h # Convert to plain hash
|
|
136
|
+
```
|
|
98
137
|
|
|
99
|
-
|
|
100
|
-
state.memory.forget("user_name")
|
|
138
|
+
## Reactive Features
|
|
101
139
|
|
|
102
|
-
|
|
103
|
-
state.memory.all # => { "user_name" => "Alice", ... }
|
|
104
|
-
```
|
|
140
|
+
Memory supports pub/sub semantics where robots can subscribe to key changes and optionally block until values become available.
|
|
105
141
|
|
|
106
|
-
###
|
|
142
|
+
### Setting Values
|
|
107
143
|
|
|
108
|
-
|
|
144
|
+
Use `memory.set(key, value)` to write a value and notify subscribers asynchronously:
|
|
109
145
|
|
|
110
146
|
```ruby
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
user_memory.remember("last_login", Time.now)
|
|
147
|
+
memory.set(:sentiment, { score: 0.8, confidence: 0.95 })
|
|
148
|
+
```
|
|
114
149
|
|
|
115
|
-
|
|
116
|
-
user_memory.recall("last_login")
|
|
150
|
+
The `[]=` operator also triggers reactive notifications for non-reserved keys:
|
|
117
151
|
|
|
118
|
-
|
|
119
|
-
|
|
152
|
+
```ruby
|
|
153
|
+
memory[:sentiment] = { score: 0.8 } # Equivalent to memory.set(:sentiment, ...)
|
|
120
154
|
```
|
|
121
155
|
|
|
122
|
-
###
|
|
156
|
+
### Blocking Reads
|
|
123
157
|
|
|
124
|
-
Use
|
|
158
|
+
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:
|
|
125
159
|
|
|
126
160
|
```ruby
|
|
127
|
-
#
|
|
128
|
-
|
|
161
|
+
# Immediate read (returns nil if missing)
|
|
162
|
+
memory.get(:sentiment)
|
|
163
|
+
|
|
164
|
+
# Block indefinitely until value exists
|
|
165
|
+
memory.get(:sentiment, wait: true)
|
|
166
|
+
|
|
167
|
+
# Block up to 30 seconds, raise AwaitTimeout if exceeded
|
|
168
|
+
memory.get(:sentiment, wait: 30)
|
|
129
169
|
|
|
130
|
-
#
|
|
131
|
-
|
|
170
|
+
# Wait for multiple keys at once
|
|
171
|
+
results = memory.get(:sentiment, :entities, :keywords, wait: 60)
|
|
172
|
+
#=> { sentiment: {...}, entities: [...], keywords: [...] }
|
|
132
173
|
```
|
|
133
174
|
|
|
134
|
-
###
|
|
175
|
+
### Subscriptions
|
|
176
|
+
|
|
177
|
+
Subscribe to key changes with async callbacks. The callback receives a `MemoryChange` object:
|
|
135
178
|
|
|
136
179
|
```ruby
|
|
137
|
-
|
|
138
|
-
|
|
180
|
+
memory.subscribe(:raw_data) do |change|
|
|
181
|
+
puts "#{change.key} changed from #{change.previous} to #{change.value}"
|
|
182
|
+
puts "Written by: #{change.writer} at #{change.timestamp}"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Subscribe to multiple keys
|
|
186
|
+
memory.subscribe(:sentiment, :entities) do |change|
|
|
187
|
+
update_dashboard(change.key, change.value)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Pattern-based subscriptions (glob-style matching)
|
|
191
|
+
memory.subscribe_pattern("analysis:*") do |change|
|
|
192
|
+
puts "Analysis key #{change.key} updated"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Unsubscribe
|
|
196
|
+
sub_id = memory.subscribe(:status) { |c| puts c.value }
|
|
197
|
+
memory.unsubscribe(sub_id)
|
|
198
|
+
|
|
199
|
+
# Check if key has subscribers
|
|
200
|
+
memory.subscribed?(:status) #=> true/false
|
|
201
|
+
```
|
|
139
202
|
|
|
140
|
-
|
|
141
|
-
state.memory.stats
|
|
142
|
-
# => { total_keys: 15, namespaces: ["user", "session"] }
|
|
203
|
+
### MemoryChange
|
|
143
204
|
|
|
144
|
-
|
|
145
|
-
state.memory.scoped("temp").clear
|
|
205
|
+
The `MemoryChange` object provides context about what changed:
|
|
146
206
|
|
|
147
|
-
|
|
148
|
-
|
|
207
|
+
```ruby
|
|
208
|
+
change.key #=> :sentiment
|
|
209
|
+
change.value #=> { score: 0.8 }
|
|
210
|
+
change.previous #=> nil (or previous value)
|
|
211
|
+
change.writer #=> "classifier" (robot name)
|
|
212
|
+
change.network_name #=> "support_pipeline"
|
|
213
|
+
change.timestamp #=> Time
|
|
214
|
+
change.created? #=> true (new key, no previous value)
|
|
215
|
+
change.updated? #=> false
|
|
216
|
+
change.deleted? #=> false
|
|
149
217
|
```
|
|
150
218
|
|
|
151
|
-
##
|
|
219
|
+
## Memory Lifecycle
|
|
220
|
+
|
|
221
|
+
### Results
|
|
152
222
|
|
|
153
223
|
Results track the history of robot executions:
|
|
154
224
|
|
|
155
225
|
```ruby
|
|
156
226
|
# Append a result
|
|
157
|
-
|
|
227
|
+
memory.append_result(robot_result)
|
|
158
228
|
|
|
159
|
-
# Get all results
|
|
160
|
-
|
|
229
|
+
# Get all results (returns a copy)
|
|
230
|
+
memory.results
|
|
161
231
|
|
|
162
|
-
# Get results from index
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
# Format for LLM conversation
|
|
166
|
-
state.format_history
|
|
232
|
+
# Get results from a specific index (for incremental persistence)
|
|
233
|
+
memory.results_from(5)
|
|
167
234
|
```
|
|
168
235
|
|
|
169
|
-
### Result History
|
|
170
|
-
|
|
171
236
|
Each `RobotResult` contains:
|
|
172
237
|
|
|
173
238
|
```ruby
|
|
174
|
-
result.robot_name
|
|
175
|
-
result.output
|
|
176
|
-
result.tool_calls
|
|
177
|
-
result.stop_reason
|
|
178
|
-
result.
|
|
239
|
+
result.robot_name # Which robot produced this
|
|
240
|
+
result.output # Array<Message> - response content
|
|
241
|
+
result.tool_calls # Array<ToolResultMessage> - tools called
|
|
242
|
+
result.stop_reason # Stop reason from LLM
|
|
243
|
+
result.last_text_content # Convenience: last text content string
|
|
244
|
+
result.has_tool_calls? # Whether any tools were called
|
|
245
|
+
result.created_at # When it was created
|
|
179
246
|
```
|
|
180
247
|
|
|
181
|
-
|
|
248
|
+
### Format History
|
|
182
249
|
|
|
183
|
-
The `
|
|
250
|
+
The `format_history` method prepares messages for LLM consumption:
|
|
184
251
|
|
|
185
252
|
```ruby
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
# Returns Array<Message> with:
|
|
189
|
-
# - System message (if present)
|
|
190
|
-
# - Alternating user/assistant messages
|
|
191
|
-
# - Tool calls and results
|
|
253
|
+
formatted = memory.format_history
|
|
254
|
+
# Returns combined messages + formatted results
|
|
192
255
|
```
|
|
193
256
|
|
|
194
|
-
|
|
257
|
+
### Merge
|
|
195
258
|
|
|
196
|
-
|
|
259
|
+
Merge additional values into memory:
|
|
197
260
|
|
|
198
261
|
```ruby
|
|
199
|
-
|
|
200
|
-
|
|
262
|
+
memory.merge!(user_id: 123, category: "billing")
|
|
263
|
+
```
|
|
201
264
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
265
|
+
### Key Management
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
memory.key?(:user_id) # Check existence
|
|
269
|
+
memory.keys # Get all non-reserved keys
|
|
270
|
+
memory.all_keys # Get all keys including reserved
|
|
271
|
+
memory.delete(:temp_data) # Delete a specific key
|
|
272
|
+
memory.clear # Clear all non-reserved keys
|
|
273
|
+
memory.reset # Reset to initial state (preserves cache)
|
|
208
274
|
```
|
|
209
275
|
|
|
210
|
-
##
|
|
276
|
+
## Cloning
|
|
211
277
|
|
|
212
|
-
Create independent copies:
|
|
278
|
+
Create independent copies of memory for isolated execution. Subscriptions are not cloned:
|
|
213
279
|
|
|
214
280
|
```ruby
|
|
215
|
-
original = RobotLab.
|
|
216
|
-
|
|
281
|
+
original = RobotLab.create_memory(data: { count: 1 })
|
|
282
|
+
cloned = original.clone
|
|
217
283
|
|
|
218
|
-
|
|
219
|
-
original
|
|
284
|
+
cloned[:count] = 2
|
|
285
|
+
original[:count] #=> still 1
|
|
220
286
|
```
|
|
221
287
|
|
|
222
288
|
## Serialization
|
|
223
289
|
|
|
224
|
-
Convert
|
|
290
|
+
Convert memory to and from hash for persistence:
|
|
225
291
|
|
|
226
292
|
```ruby
|
|
227
293
|
# To hash
|
|
228
|
-
hash =
|
|
229
|
-
|
|
294
|
+
hash = memory.to_h
|
|
295
|
+
#=> {
|
|
296
|
+
# data: { ... },
|
|
297
|
+
# results: [...],
|
|
298
|
+
# messages: [...],
|
|
299
|
+
# session_id: "abc123",
|
|
300
|
+
# custom: { my_key: "value" }
|
|
301
|
+
# }
|
|
302
|
+
|
|
303
|
+
# To JSON
|
|
304
|
+
json = memory.to_json
|
|
230
305
|
|
|
231
306
|
# From hash
|
|
232
|
-
|
|
307
|
+
memory = Memory.from_hash(hash)
|
|
233
308
|
```
|
|
234
309
|
|
|
235
|
-
|
|
310
|
+
## Semantic Cache
|
|
311
|
+
|
|
312
|
+
Memory includes a semantic cache via `RubyLLM::SemanticCache` that reduces costs and latency by returning cached responses for semantically equivalent queries:
|
|
236
313
|
|
|
237
314
|
```ruby
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
],
|
|
248
|
-
thread_id: "thread_123"
|
|
249
|
-
}
|
|
315
|
+
# Using the cache with fetch
|
|
316
|
+
response = memory.cache.fetch("What is Ruby?") do
|
|
317
|
+
RubyLLM.chat.ask("What is Ruby?")
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Wrapping a chat instance
|
|
321
|
+
chat = memory.cache.wrap(RubyLLM.chat(model: "gpt-4o"))
|
|
322
|
+
chat.ask("What is Ruby?") # Cached on semantic similarity
|
|
250
323
|
```
|
|
251
324
|
|
|
252
|
-
|
|
325
|
+
Caching can be disabled per-memory or per-robot:
|
|
253
326
|
|
|
254
|
-
|
|
327
|
+
```ruby
|
|
328
|
+
memory = RobotLab.create_memory(enable_cache: false)
|
|
329
|
+
robot = RobotLab.build(name: "bot", system_prompt: "...", enable_cache: false)
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## Backend Options
|
|
333
|
+
|
|
334
|
+
Memory defaults to a Hash-based backend but can use Redis for distributed scenarios:
|
|
255
335
|
|
|
256
336
|
```ruby
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
thread_id: "thread_123",
|
|
260
|
-
system_prompt: "Respond in Spanish", # Augment system prompt
|
|
261
|
-
metadata: {
|
|
262
|
-
user_id: "user_456",
|
|
263
|
-
source: "web_chat"
|
|
264
|
-
}
|
|
265
|
-
)
|
|
337
|
+
# Auto-detect (uses Redis if available, falls back to Hash)
|
|
338
|
+
memory = Memory.new(backend: :auto)
|
|
266
339
|
|
|
267
|
-
|
|
268
|
-
|
|
340
|
+
# Force Hash backend
|
|
341
|
+
memory = Memory.new(backend: :hash)
|
|
269
342
|
|
|
270
|
-
|
|
343
|
+
# Force Redis backend
|
|
344
|
+
memory = Memory.new(backend: :redis)
|
|
271
345
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
| `metadata` | Custom key-value data |
|
|
278
|
-
| `id` | Unique message identifier |
|
|
279
|
-
| `created_at` | Timestamp |
|
|
346
|
+
# Check backend
|
|
347
|
+
memory.redis? #=> true/false
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Redis is configured via `RobotLab.config.redis` or the `REDIS_URL` environment variable.
|
|
280
351
|
|
|
281
352
|
## Best Practices
|
|
282
353
|
|
|
283
354
|
### 1. Use Memory for Cross-Robot Data
|
|
284
355
|
|
|
285
356
|
```ruby
|
|
286
|
-
#
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
state.memory.remember("classification", "billing")
|
|
293
|
-
# Later robot reads it directly
|
|
357
|
+
# In a network, robots share memory automatically.
|
|
358
|
+
# Robot A writes:
|
|
359
|
+
memory.set(:classification, "billing")
|
|
360
|
+
|
|
361
|
+
# Robot B reads:
|
|
362
|
+
category = memory.get(:classification)
|
|
294
363
|
```
|
|
295
364
|
|
|
296
|
-
### 2.
|
|
365
|
+
### 2. Use Blocking Reads for Concurrent Pipelines
|
|
297
366
|
|
|
298
367
|
```ruby
|
|
299
|
-
#
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
# User preferences
|
|
303
|
-
user = state.memory.scoped("user:#{user_id}")
|
|
304
|
-
|
|
305
|
-
# Temporary working data
|
|
306
|
-
temp = state.memory.scoped("temp")
|
|
368
|
+
# When robots run in parallel, use blocking reads
|
|
369
|
+
# to synchronize on shared data:
|
|
370
|
+
results = memory.get(:sentiment, :entities, wait: 60)
|
|
307
371
|
```
|
|
308
372
|
|
|
309
373
|
### 3. Keep Data Minimal
|
|
310
374
|
|
|
311
375
|
```ruby
|
|
312
|
-
#
|
|
313
|
-
|
|
376
|
+
# Store references instead of large objects
|
|
377
|
+
memory[:response_id] = response.id # Preferred
|
|
378
|
+
# memory[:huge_response] = api_response # Avoid
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### 4. Reset Between Independent Runs
|
|
314
382
|
|
|
315
|
-
|
|
316
|
-
|
|
383
|
+
```ruby
|
|
384
|
+
network.reset_memory
|
|
385
|
+
result = network.run(message: "New conversation")
|
|
317
386
|
```
|
|
318
387
|
|
|
319
388
|
## Next Steps
|
|
320
389
|
|
|
321
|
-
- [
|
|
322
|
-
- [History Guide](../guides/history.md) - Persisting state
|
|
390
|
+
- [Network Orchestration](network-orchestration.md) - How networks share memory
|
|
323
391
|
- [Message Flow](message-flow.md) - How messages are processed
|