robot_lab 0.0.1 → 0.0.4

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.
Files changed (145) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/deploy-github-pages.yml +9 -9
  3. data/.irbrc +6 -0
  4. data/CHANGELOG.md +90 -0
  5. data/README.md +203 -46
  6. data/Rakefile +70 -1
  7. data/docs/api/core/index.md +12 -0
  8. data/docs/api/core/robot.md +478 -130
  9. data/docs/api/core/tool.md +205 -209
  10. data/docs/api/history/active-record-adapter.md +174 -94
  11. data/docs/api/history/config.md +186 -93
  12. data/docs/api/history/index.md +57 -61
  13. data/docs/api/history/thread-manager.md +123 -73
  14. data/docs/api/mcp/client.md +119 -48
  15. data/docs/api/mcp/index.md +75 -60
  16. data/docs/api/mcp/server.md +120 -136
  17. data/docs/api/mcp/transports.md +172 -184
  18. data/docs/api/streaming/context.md +157 -74
  19. data/docs/api/streaming/events.md +114 -166
  20. data/docs/api/streaming/index.md +74 -72
  21. data/docs/architecture/core-concepts.md +361 -112
  22. data/docs/architecture/index.md +97 -59
  23. data/docs/architecture/message-flow.md +138 -129
  24. data/docs/architecture/network-orchestration.md +197 -50
  25. data/docs/architecture/robot-execution.md +199 -146
  26. data/docs/architecture/state-management.md +255 -187
  27. data/docs/concepts.md +312 -48
  28. data/docs/examples/basic-chat.md +89 -77
  29. data/docs/examples/index.md +222 -47
  30. data/docs/examples/mcp-server.md +207 -203
  31. data/docs/examples/multi-robot-network.md +129 -35
  32. data/docs/examples/rails-application.md +159 -160
  33. data/docs/examples/tool-usage.md +295 -204
  34. data/docs/getting-started/configuration.md +275 -162
  35. data/docs/getting-started/index.md +1 -1
  36. data/docs/getting-started/installation.md +22 -13
  37. data/docs/getting-started/quick-start.md +166 -121
  38. data/docs/guides/building-robots.md +417 -212
  39. data/docs/guides/creating-networks.md +94 -24
  40. data/docs/guides/mcp-integration.md +152 -113
  41. data/docs/guides/memory.md +220 -164
  42. data/docs/guides/streaming.md +80 -110
  43. data/docs/guides/using-tools.md +259 -212
  44. data/docs/index.md +50 -37
  45. data/examples/01_simple_robot.rb +6 -9
  46. data/examples/02_tools.rb +6 -9
  47. data/examples/03_network.rb +13 -14
  48. data/examples/04_mcp.rb +5 -8
  49. data/examples/05_streaming.rb +5 -8
  50. data/examples/06_prompt_templates.rb +42 -37
  51. data/examples/07_network_memory.rb +13 -14
  52. data/examples/08_llm_config.rb +140 -0
  53. data/examples/09_chaining.rb +223 -0
  54. data/examples/10_memory.rb +331 -0
  55. data/examples/11_network_introspection.rb +230 -0
  56. data/examples/12_message_bus.rb +74 -0
  57. data/examples/13_spawn.rb +90 -0
  58. data/examples/14_rusty_circuit/comic.rb +143 -0
  59. data/examples/14_rusty_circuit/display.rb +203 -0
  60. data/examples/14_rusty_circuit/heckler.rb +57 -0
  61. data/examples/14_rusty_circuit/open_mic.rb +121 -0
  62. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +20 -0
  63. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +23 -0
  64. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +20 -0
  65. data/examples/14_rusty_circuit/scout.rb +173 -0
  66. data/examples/14_rusty_circuit/scout_notes.md +89 -0
  67. data/examples/14_rusty_circuit/show.log +234 -0
  68. data/examples/15_memory_network_and_bus/editor_in_chief.rb +24 -0
  69. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +206 -0
  70. data/examples/15_memory_network_and_bus/linux_writer.rb +80 -0
  71. data/examples/15_memory_network_and_bus/os_editor.rb +46 -0
  72. data/examples/15_memory_network_and_bus/os_writer.rb +46 -0
  73. data/examples/15_memory_network_and_bus/output/combined_article.md +13 -0
  74. data/examples/15_memory_network_and_bus/output/final_article.md +15 -0
  75. data/examples/15_memory_network_and_bus/output/linux_draft.md +5 -0
  76. data/examples/15_memory_network_and_bus/output/mac_draft.md +7 -0
  77. data/examples/15_memory_network_and_bus/output/memory.json +13 -0
  78. data/examples/15_memory_network_and_bus/output/revision_1.md +19 -0
  79. data/examples/15_memory_network_and_bus/output/revision_2.md +15 -0
  80. data/examples/15_memory_network_and_bus/output/windows_draft.md +7 -0
  81. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +13 -0
  82. data/examples/15_memory_network_and_bus/prompts/os_chief.md +13 -0
  83. data/examples/15_memory_network_and_bus/prompts/os_editor.md +13 -0
  84. data/examples/README.md +197 -0
  85. data/examples/prompts/{assistant/system.txt.erb → assistant.md} +3 -0
  86. data/examples/prompts/{billing/system.txt.erb → billing.md} +3 -0
  87. data/examples/prompts/{classifier/system.txt.erb → classifier.md} +3 -0
  88. data/examples/prompts/comedian.md +6 -0
  89. data/examples/prompts/comedy_critic.md +10 -0
  90. data/examples/prompts/configurable.md +9 -0
  91. data/examples/prompts/dispatcher.md +12 -0
  92. data/examples/prompts/{entity_extractor/system.txt.erb → entity_extractor.md} +3 -0
  93. data/examples/prompts/{escalation/system.txt.erb → escalation.md} +7 -0
  94. data/examples/prompts/frontmatter_mcp_test.md +9 -0
  95. data/examples/prompts/frontmatter_named_test.md +5 -0
  96. data/examples/prompts/frontmatter_tools_test.md +6 -0
  97. data/examples/prompts/{general/system.txt.erb → general.md} +3 -0
  98. data/examples/prompts/{github_assistant/system.txt.erb → github_assistant.md} +8 -0
  99. data/examples/prompts/{helper/system.txt.erb → helper.md} +3 -0
  100. data/examples/prompts/{keyword_extractor/system.txt.erb → keyword_extractor.md} +3 -0
  101. data/examples/prompts/llm_config_demo.md +20 -0
  102. data/examples/prompts/{order_support/system.txt.erb → order_support.md} +8 -0
  103. data/examples/prompts/os_advocate.md +13 -0
  104. data/examples/prompts/os_chief.md +13 -0
  105. data/examples/prompts/os_editor.md +13 -0
  106. data/examples/prompts/{product_support/system.txt.erb → product_support.md} +7 -0
  107. data/examples/prompts/{sentiment_analyzer/system.txt.erb → sentiment_analyzer.md} +3 -0
  108. data/examples/prompts/{synthesizer/system.txt.erb → synthesizer.md} +3 -0
  109. data/examples/prompts/{technical/system.txt.erb → technical.md} +3 -0
  110. data/examples/prompts/{triage/system.txt.erb → triage.md} +6 -0
  111. data/lib/generators/robot_lab/templates/initializer.rb.tt +1 -1
  112. data/lib/robot_lab/adapters/openai.rb +2 -1
  113. data/lib/robot_lab/ask_user.rb +75 -0
  114. data/lib/robot_lab/config/defaults.yml +121 -0
  115. data/lib/robot_lab/config.rb +183 -0
  116. data/lib/robot_lab/error.rb +6 -0
  117. data/lib/robot_lab/mcp/client.rb +1 -1
  118. data/lib/robot_lab/memory.rb +2 -2
  119. data/lib/robot_lab/robot.rb +523 -249
  120. data/lib/robot_lab/robot_message.rb +44 -0
  121. data/lib/robot_lab/robot_result.rb +1 -0
  122. data/lib/robot_lab/robotic_model.rb +1 -1
  123. data/lib/robot_lab/streaming/context.rb +1 -1
  124. data/lib/robot_lab/tool.rb +108 -172
  125. data/lib/robot_lab/tool_config.rb +1 -1
  126. data/lib/robot_lab/tool_manifest.rb +2 -18
  127. data/lib/robot_lab/version.rb +1 -1
  128. data/lib/robot_lab.rb +66 -55
  129. metadata +107 -116
  130. data/examples/prompts/assistant/user.txt.erb +0 -1
  131. data/examples/prompts/billing/user.txt.erb +0 -1
  132. data/examples/prompts/classifier/user.txt.erb +0 -1
  133. data/examples/prompts/entity_extractor/user.txt.erb +0 -3
  134. data/examples/prompts/escalation/user.txt.erb +0 -34
  135. data/examples/prompts/general/user.txt.erb +0 -1
  136. data/examples/prompts/github_assistant/user.txt.erb +0 -1
  137. data/examples/prompts/helper/user.txt.erb +0 -1
  138. data/examples/prompts/keyword_extractor/user.txt.erb +0 -3
  139. data/examples/prompts/order_support/user.txt.erb +0 -22
  140. data/examples/prompts/product_support/user.txt.erb +0 -32
  141. data/examples/prompts/sentiment_analyzer/user.txt.erb +0 -3
  142. data/examples/prompts/synthesizer/user.txt.erb +0 -15
  143. data/examples/prompts/technical/user.txt.erb +0 -1
  144. data/examples/prompts/triage/user.txt.erb +0 -17
  145. data/lib/robot_lab/configuration.rb +0 -143
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example 8: LLM Configuration via MywayConfig
5
+ #
6
+ # Demonstrates how RobotLab uses MywayConfig for environment-specific
7
+ # configuration (similar to Rails database.yml).
8
+ #
9
+ # Configuration is loaded from multiple sources in priority order:
10
+ # 1. Bundled defaults (lib/robot_lab/config/defaults.yml)
11
+ # 2. Environment-specific overrides (development, test, production)
12
+ # 3. XDG user config (~/.config/robot_lab/config.yml)
13
+ # 4. Project config (./config/robot_lab.yml)
14
+ # 5. Environment variables (ROBOT_LAB_*)
15
+ # 6. Template front matter (model, temperature, etc. in .md YAML header)
16
+ # 7. Constructor parameters (model:, temperature:, etc. passed to Robot.new)
17
+ # 8. with_* methods (runtime chaining, e.g. robot.with_temperature(0.9))
18
+ # 9. Run-time context (kwargs passed to robot.run that re-render the template)
19
+ #
20
+ # Environment is determined by ROBOT_LAB_ENV, RAILS_ENV, or RACK_ENV.
21
+ #
22
+ # Environment variable examples:
23
+ # ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...
24
+ # ROBOT_LAB_RUBY_LLM__MODEL=gpt-4
25
+ # ROBOT_LAB_RUBY_LLM__REQUEST_TIMEOUT=180
26
+ #
27
+ # Usage:
28
+ # ANTHROPIC_API_KEY=your_key ruby examples/08_llm_config.rb
29
+ # ROBOT_LAB_ENV=test ANTHROPIC_API_KEY=your_key ruby examples/08_llm_config.rb
30
+ # ROBOT_LAB_ENV=production ANTHROPIC_API_KEY=your_key ruby examples/08_llm_config.rb
31
+
32
+ # Configure template path before loading (MywayConfig reads env vars on init)
33
+ ENV['ROBOT_LAB_TEMPLATE_PATH'] ||= File.join(__dir__, "prompts")
34
+
35
+ require_relative "../lib/robot_lab"
36
+
37
+ # =============================================================================
38
+ # Demonstration
39
+ # =============================================================================
40
+
41
+ puts "=" * 70
42
+ puts "Example 8: LLM Configuration via MywayConfig"
43
+ puts "=" * 70
44
+ puts
45
+
46
+ # --- Gather environment from the user via AskUser ---
47
+ ask = RobotLab::AskUser.new
48
+ env = ask.call(
49
+ "question" => "Which environment should the demo run in?",
50
+ "choices" => %w[development test production],
51
+ "default" => ENV['ROBOT_LAB_ENV'] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development"
52
+ )
53
+ puts
54
+
55
+ # Display configuration values (hiding sensitive keys)
56
+ config = RobotLab.config
57
+
58
+ puts "Core Settings:"
59
+ puts " max_iterations: #{config.max_iterations}"
60
+ puts " streaming_enabled: #{config.streaming_enabled}"
61
+ puts " template_path: #{config.template_path || '(default)'}"
62
+ puts
63
+
64
+ puts "RubyLLM Settings:"
65
+ puts " model: #{config.ruby_llm.model}"
66
+ puts " provider: #{config.ruby_llm.provider}"
67
+ puts " request_timeout: #{config.ruby_llm.request_timeout}s"
68
+ puts " max_retries: #{config.ruby_llm.max_retries}"
69
+ puts " log_level: #{config.ruby_llm.log_level}"
70
+
71
+ # Show API key status without revealing values
72
+ api_key = config.ruby_llm.anthropic_api_key
73
+ puts " anthropic_api_key: #{api_key ? '[SET via config]' : (ENV['ANTHROPIC_API_KEY'] ? '[SET via env]' : '(not set)')}"
74
+ puts
75
+
76
+ puts "-" * 70
77
+ puts "Configuration hierarchy (highest priority first):"
78
+ puts
79
+ puts " Per-Robot (override global settings for a specific robot):"
80
+ puts " 9. Run-time context: kwargs to robot.run re-render template"
81
+ puts " 8. with_* methods: robot.with_temperature(0.9).ask(...)"
82
+ puts " 7. Constructor params: Robot.new(model: ..., temperature: ...)"
83
+ puts " 6. Template front matter: model, temperature, etc. in .md YAML header"
84
+ puts
85
+ puts " Global (apply to all robots unless overridden):"
86
+ puts " 5. Environment variables: ROBOT_LAB_RUBY_LLM__MODEL, etc."
87
+ puts " 4. Project config: ./config/robot_lab.yml"
88
+ puts " 3. User config: ~/.config/robot_lab/config.yml"
89
+ puts " 2. Environment overrides: #{env} section in defaults.yml"
90
+ puts " 1. Bundled defaults: lib/robot_lab/config/defaults.yml"
91
+ puts "-" * 70
92
+ puts
93
+
94
+ # Create a robot using the configuration
95
+ robot = RobotLab.build(
96
+ name: "config_demo",
97
+ template: :llm_config_demo,
98
+ local_tools: [RobotLab::AskUser],
99
+ context: {
100
+ environment: env,
101
+ model: config.ruby_llm.model,
102
+ provider: config.ruby_llm.provider.to_s
103
+ }
104
+ )
105
+
106
+ puts "Running robot with #{env} configuration..."
107
+ puts "Model: #{robot.model}"
108
+ puts
109
+
110
+ # Run the robot
111
+ result = robot.run(
112
+ "Briefly explain how environment-specific LLM configuration works " \
113
+ "and why it's useful."
114
+ )
115
+
116
+ # Display the result
117
+ puts "Response:"
118
+ result.output.each do |message|
119
+ puts message.content if message.respond_to?(:content)
120
+ end
121
+
122
+ puts <<~FOOTER
123
+
124
+ #{"=" * 70}
125
+ Configuration demonstrated successfully!
126
+
127
+ Configuration flows through two layers:
128
+ Global (MywayConfig): YAML defaults, env overrides, XDG, env vars
129
+ Per-Robot: template front matter, constructor params, with_* methods
130
+
131
+ Example environment variable overrides:
132
+ ROBOT_LAB_RUBY_LLM__MODEL=gpt-4
133
+ ROBOT_LAB_RUBY_LLM__REQUEST_TIMEOUT=180
134
+ ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...
135
+
136
+ Try running with different environments:
137
+ ROBOT_LAB_ENV=test ruby examples/08_llm_config.rb
138
+ ROBOT_LAB_ENV=production ruby examples/08_llm_config.rb
139
+ #{"=" * 70}
140
+ FOOTER
@@ -0,0 +1,223 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example 9: Robot Chaining & Reconfiguration
5
+ #
6
+ # Demonstrates the Robot API surface for runtime configuration without
7
+ # making any LLM calls. Covers:
8
+ # - with_* method chaining
9
+ # - update() for reconfiguration
10
+ # - to_h introspection (via amazing_print)
11
+ # - Config diffs between steps (via hashdiff)
12
+ # - Template front matter config keys
13
+ # - Constructor params overriding front matter
14
+ # - Config hierarchy in action
15
+ # - AskUser tool for gathering template parameters interactively
16
+ #
17
+ # Usage:
18
+ # bundle exec ruby examples/09_chaining.rb
19
+
20
+ # Configure template path before loading
21
+ ENV['ROBOT_LAB_TEMPLATE_PATH'] ||= File.join(__dir__, "prompts")
22
+
23
+ require_relative "../lib/robot_lab"
24
+ require "amazing_print"
25
+ require "hashdiff"
26
+ require "stringio"
27
+
28
+ # Show a config snapshot and the diff from the previous snapshot.
29
+ # Returns the current hash for use as the next "previous" snapshot.
30
+ def show_config(robot, previous_config = nil)
31
+ current = robot.to_h
32
+ ap current
33
+
34
+ if previous_config
35
+ diff = Hashdiff.diff(previous_config, current)
36
+ if diff.empty?
37
+ puts " (no changes to to_h)"
38
+ else
39
+ puts
40
+ puts " Diff from previous:"
41
+ diff.each do |change|
42
+ op, path, *values = change
43
+ case op
44
+ when "+"
45
+ puts " + #{path}: #{values.first.inspect}"
46
+ when "-"
47
+ puts " - #{path}: #{values.first.inspect}"
48
+ when "~"
49
+ puts " ~ #{path}: #{values.first.inspect} -> #{values.last.inspect}"
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ current
56
+ end
57
+
58
+ puts "=" * 70
59
+ puts "Example 9: Robot Chaining & Reconfiguration"
60
+ puts "=" * 70
61
+ puts
62
+
63
+ # =============================================================================
64
+ # Section 1: Template front matter config
65
+ # =============================================================================
66
+
67
+ puts "--- Section 1: Template Front Matter Config ---"
68
+ puts
69
+
70
+ # The 'configurable' template sets temperature: 0.3 and max_tokens: 200
71
+ # via YAML front matter. These are applied automatically.
72
+ robot = RobotLab.build(
73
+ name: "chameleon",
74
+ template: :configurable,
75
+ context: { task_type: "analysis" }
76
+ )
77
+
78
+ puts "Robot created with :configurable template"
79
+ prev = show_config(robot)
80
+ puts
81
+
82
+ # =============================================================================
83
+ # Section 2: with_* method chaining
84
+ # =============================================================================
85
+
86
+ puts "--- Section 2: with_* Method Chaining ---"
87
+ puts
88
+
89
+ # with_* methods return self, enabling fluent chaining.
90
+ # These override whatever the template set.
91
+ robot.with_temperature(0.9)
92
+
93
+ puts "After chaining with_temperature(0.9):"
94
+ prev = show_config(robot, prev)
95
+ puts
96
+
97
+ # =============================================================================
98
+ # Section 3: update() for reconfiguration
99
+ # =============================================================================
100
+
101
+ puts "--- Section 3: update() for Reconfiguration ---"
102
+ puts
103
+
104
+ # update() can swap the template, model, temperature, and other settings.
105
+ robot.update(template: :assistant, temperature: 0.5)
106
+
107
+ puts "After update(template: :assistant, temperature: 0.5):"
108
+ prev = show_config(robot, prev)
109
+ puts
110
+
111
+ # Swap back with context
112
+ robot.update(template: :configurable, context: { task_type: "creative" })
113
+
114
+ puts "After update(template: :configurable, context: { task_type: 'creative' }):"
115
+ prev = show_config(robot, prev)
116
+ puts
117
+
118
+ # =============================================================================
119
+ # Section 4: Constructor params override front matter
120
+ # =============================================================================
121
+
122
+ puts "--- Section 4: Constructor Params Override Front Matter ---"
123
+ puts
124
+
125
+ # The configurable template sets temperature: 0.3 in front matter.
126
+ # Passing temperature: 0.9 to the constructor overrides it.
127
+ robot2 = RobotLab.build(
128
+ name: "override_demo",
129
+ template: :configurable,
130
+ context: { task_type: "creative" },
131
+ temperature: 0.9
132
+ )
133
+
134
+ puts "Template sets temperature: 0.3, constructor passes temperature: 0.9"
135
+ prev2 = show_config(robot2)
136
+ puts
137
+
138
+ # =============================================================================
139
+ # Section 5: Bare robot with chaining
140
+ # =============================================================================
141
+
142
+ puts "--- Section 5: Bare Robot with Chaining ---"
143
+ puts
144
+
145
+ # A bare robot has no template. Configure entirely via chaining.
146
+ bare = RobotLab.build(name: "bare")
147
+
148
+ puts "Bare robot before chaining:"
149
+ prev_bare = show_config(bare)
150
+ puts
151
+
152
+ bare
153
+ .with_instructions("You are a helpful assistant.")
154
+ .with_temperature(0.5)
155
+
156
+ puts "After with_instructions(...).with_temperature(0.5):"
157
+ prev_bare = show_config(bare, prev_bare)
158
+ puts
159
+
160
+ # =============================================================================
161
+ # Section 6: with_template() on an existing robot
162
+ # =============================================================================
163
+
164
+ puts "--- Section 6: with_template() on Existing Robot ---"
165
+ puts
166
+
167
+ # You can apply a template to a robot after creation.
168
+ bare.with_template(:helper)
169
+
170
+ puts "After bare.with_template(:helper):"
171
+ show_config(bare, prev_bare)
172
+ puts
173
+
174
+ # =============================================================================
175
+ # Section 7: AskUser tool for gathering template parameters
176
+ # =============================================================================
177
+
178
+ puts "--- Section 7: AskUser for Template Parameters ---"
179
+ puts
180
+ puts "The :configurable template declares `task_type: general` in its front"
181
+ puts "matter. That default is offered to the user — they can accept it by"
182
+ puts "pressing Enter or type something else. Parameters with null values"
183
+ puts "have no default and require user input."
184
+ puts
185
+
186
+ # Build a robot with AskUser — the template's task_type default ("general")
187
+ # is offered to the user, who can accept or override it.
188
+ interactive = RobotLab.build(
189
+ name: "interactive_demo",
190
+ template: :configurable,
191
+ local_tools: [RobotLab::AskUser]
192
+ )
193
+
194
+ puts "Robot 'interactive_demo' has AskUser in its tools."
195
+ puts "The template renders with the default: task_type = \"general\""
196
+ puts "When run, the robot can call ask_user to let the user confirm"
197
+ puts "or change the value."
198
+ puts
199
+
200
+ # Simulate what the user would see (no LLM call needed)
201
+ output = StringIO.new
202
+ demo_tool = RobotLab::AskUser.new(robot: interactive)
203
+ interactive.output = output
204
+ interactive.input = StringIO.new("2\n")
205
+
206
+ result = demo_tool.call(
207
+ "question" => "What type of task should I optimize for?",
208
+ "choices" => %w[general analysis creative coding research],
209
+ "default" => "general"
210
+ )
211
+
212
+ puts output.string
213
+ puts "User selected: #{result}"
214
+ puts
215
+ puts "The robot would re-render the template with task_type: \"#{result}\""
216
+ puts "producing: \"You are a precise assistant optimized for #{result} tasks.\""
217
+ puts
218
+ puts "If the user had pressed Enter, the default \"general\" would be kept."
219
+ puts
220
+
221
+ puts "=" * 70
222
+ puts "All sections completed without any LLM calls."
223
+ puts "=" * 70
@@ -0,0 +1,331 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example 10: Advanced Memory Operations
5
+ #
6
+ # Demonstrates the Memory API without making any LLM calls. Covers:
7
+ # - StateProxy for method-style data access
8
+ # - Key subscriptions with MemoryChange objects
9
+ # - Pattern subscriptions
10
+ # - Unsubscribe
11
+ # - Key enumeration (keys, all_keys, key?)
12
+ # - Serialization (to_h, from_hash, to_json) with amazing_print
13
+ # - Serialization round-trip verification with hashdiff
14
+ # - Clone for isolated copies
15
+ # - Delete with reserved key protection
16
+ # - Clear vs reset
17
+ #
18
+ # Usage:
19
+ # bundle exec ruby examples/10_memory.rb
20
+
21
+ # Configure template path before loading
22
+ ENV['ROBOT_LAB_TEMPLATE_PATH'] ||= File.join(__dir__, "prompts")
23
+
24
+ require_relative "../lib/robot_lab"
25
+ require "json"
26
+ require "amazing_print"
27
+ require "hashdiff"
28
+
29
+ puts "=" * 70
30
+ puts "Example 10: Advanced Memory Operations"
31
+ puts "=" * 70
32
+ puts
33
+
34
+ # =============================================================================
35
+ # Section 1: StateProxy for method-style access
36
+ # =============================================================================
37
+
38
+ puts "--- Section 1: StateProxy for Method-Style Access ---"
39
+ puts
40
+
41
+ memory = RobotLab.create_memory(
42
+ data: { category: nil, priority: "low" },
43
+ enable_cache: false
44
+ )
45
+
46
+ puts "Initial data:"
47
+ ap memory.data.to_h
48
+ puts
49
+
50
+ # StateProxy allows method-style access to the :data hash
51
+ memory.data.category = "billing"
52
+
53
+ puts "After memory.data.category = 'billing':"
54
+ ap memory.data.to_h
55
+ puts
56
+
57
+ # Bracket-style also works
58
+ memory.data[:priority] = "high"
59
+ puts "After memory.data[:priority] = 'high':"
60
+ ap memory.data.to_h
61
+ puts
62
+
63
+ # =============================================================================
64
+ # Section 2: Subscriptions and MemoryChange
65
+ # =============================================================================
66
+
67
+ puts "--- Section 2: Subscriptions and MemoryChange ---"
68
+ puts
69
+
70
+ changes = []
71
+
72
+ sub_id = memory.subscribe(:status) do |change|
73
+ changes << change
74
+ end
75
+
76
+ puts "Subscribed to :status (sub_id: #{sub_id[0..7]}...)"
77
+
78
+ # Set current_writer so the MemoryChange knows who wrote
79
+ memory.current_writer = "classifier"
80
+ memory.set(:status, "processing")
81
+
82
+ # Give the async callback time to fire
83
+ sleep 0.1
84
+
85
+ if changes.any?
86
+ change = changes.first
87
+ puts "Change received:"
88
+ ap change.to_h
89
+ puts " created?: #{change.created?}"
90
+ puts " updated?: #{change.updated?}"
91
+ end
92
+
93
+ # Update the same key to see an 'updated' change
94
+ memory.set(:status, "complete")
95
+ sleep 0.1
96
+
97
+ if changes.size > 1
98
+ puts
99
+ puts "Second change (update):"
100
+ ap changes.last.to_h
101
+ puts " created?: #{changes.last.created?}"
102
+ puts " updated?: #{changes.last.updated?}"
103
+ end
104
+ puts
105
+
106
+ # =============================================================================
107
+ # Section 3: Pattern subscriptions
108
+ # =============================================================================
109
+
110
+ puts "--- Section 3: Pattern Subscriptions ---"
111
+ puts
112
+
113
+ pattern_changes = []
114
+
115
+ pattern_sub_id = memory.subscribe_pattern("analysis:*") do |change|
116
+ pattern_changes << change
117
+ end
118
+
119
+ puts "Subscribed to pattern 'analysis:*'"
120
+
121
+ memory.current_writer = "analyst"
122
+ memory.set(:"analysis:sentiment", { score: 0.8 })
123
+ memory.set(:"analysis:entities", ["Ruby", "LLM"])
124
+ memory.set(:unrelated_key, "ignored")
125
+
126
+ sleep 0.1
127
+
128
+ puts "Pattern matched #{pattern_changes.size} changes (expected 2):"
129
+ pattern_changes.each do |change|
130
+ ap({ change.key => change.value })
131
+ end
132
+ puts
133
+
134
+ # =============================================================================
135
+ # Section 4: Unsubscribe
136
+ # =============================================================================
137
+
138
+ puts "--- Section 4: Unsubscribe ---"
139
+ puts
140
+
141
+ count_before = changes.size
142
+ memory.unsubscribe(sub_id)
143
+ puts "Unsubscribed from :status"
144
+
145
+ memory.set(:status, "archived")
146
+ sleep 0.1
147
+
148
+ puts "Changes after unsubscribe: #{changes.size} (was #{count_before}, should be same)"
149
+ puts
150
+
151
+ # =============================================================================
152
+ # Section 5: Key management
153
+ # =============================================================================
154
+
155
+ puts "--- Section 5: Key Management ---"
156
+ puts
157
+
158
+ puts "memory.keys (non-reserved):"
159
+ ap memory.keys
160
+ puts
161
+
162
+ puts "memory.all_keys (includes reserved: data, results, messages, session_id, cache):"
163
+ ap memory.all_keys
164
+ puts
165
+
166
+ puts "memory.key?(:status) = #{memory.key?(:status)}"
167
+ puts "memory.key?(:nope) = #{memory.key?(:nope)}"
168
+ puts
169
+
170
+ # =============================================================================
171
+ # Section 6: Serialization round-trip
172
+ # =============================================================================
173
+
174
+ puts "--- Section 6: Serialization Round-Trip ---"
175
+ puts
176
+
177
+ hash = memory.to_h
178
+ puts "memory.to_h:"
179
+ ap hash
180
+ puts
181
+
182
+ json_str = memory.to_json
183
+ puts "memory.to_json length: #{json_str.length} chars"
184
+ puts
185
+
186
+ # Round-trip via from_hash
187
+ restored = RobotLab::Memory.from_hash(hash)
188
+
189
+ puts "Restored memory from hash:"
190
+ ap restored.to_h
191
+ puts
192
+
193
+ # Use hashdiff to verify the round-trip preserved everything
194
+ diff = Hashdiff.diff(hash, restored.to_h)
195
+ if diff.empty?
196
+ puts "Round-trip verification: PERFECT (no differences)"
197
+ else
198
+ puts "Round-trip differences:"
199
+ diff.each do |change|
200
+ op, path, *values = change
201
+ case op
202
+ when "+"
203
+ puts " + #{path}: #{values.first.inspect}"
204
+ when "-"
205
+ puts " - #{path}: #{values.first.inspect}"
206
+ when "~"
207
+ puts " ~ #{path}: #{values.first.inspect} -> #{values.last.inspect}"
208
+ end
209
+ end
210
+ end
211
+ puts
212
+
213
+ # =============================================================================
214
+ # Section 7: Clone for isolation
215
+ # =============================================================================
216
+
217
+ puts "--- Section 7: Clone for Isolation ---"
218
+ puts
219
+
220
+ cloned = memory.clone
221
+ cloned.set(:isolated_key, "only in clone")
222
+
223
+ puts "cloned.key?(:isolated_key) = #{cloned.key?(:isolated_key)}"
224
+ puts "memory.key?(:isolated_key) = #{memory.key?(:isolated_key)} (isolated!)"
225
+ puts
226
+
227
+ # Show exactly what differs between clone and original
228
+ diff = Hashdiff.diff(memory.to_h, cloned.to_h)
229
+ puts "Diff (original vs clone):"
230
+ diff.each do |change|
231
+ op, path, *values = change
232
+ case op
233
+ when "+"
234
+ puts " + #{path}: #{values.first.inspect}"
235
+ when "-"
236
+ puts " - #{path}: #{values.first.inspect}"
237
+ when "~"
238
+ puts " ~ #{path}: #{values.first.inspect} -> #{values.last.inspect}"
239
+ end
240
+ end
241
+ puts
242
+
243
+ # =============================================================================
244
+ # Section 8: Delete and reserved key protection
245
+ # =============================================================================
246
+
247
+ puts "--- Section 8: Delete and Reserved Key Protection ---"
248
+ puts
249
+
250
+ before_delete = memory.to_h
251
+ memory.delete(:status)
252
+
253
+ puts "After memory.delete(:status):"
254
+ diff = Hashdiff.diff(before_delete, memory.to_h)
255
+ diff.each do |change|
256
+ op, path, *values = change
257
+ case op
258
+ when "-"
259
+ puts " - #{path}: #{values.first.inspect}"
260
+ end
261
+ end
262
+ puts
263
+
264
+ begin
265
+ memory.delete(:data)
266
+ rescue ArgumentError => e
267
+ puts "memory.delete(:data) raises: #{e.message}"
268
+ end
269
+ puts
270
+
271
+ # =============================================================================
272
+ # Section 9: Clear vs Reset
273
+ # =============================================================================
274
+
275
+ puts "--- Section 9: Clear vs Reset ---"
276
+ puts
277
+
278
+ memory.set(:temp1, "value1")
279
+ memory.set(:temp2, "value2")
280
+
281
+ puts "Before clear:"
282
+ before_clear = memory.to_h
283
+ ap before_clear
284
+ puts
285
+
286
+ memory.clear
287
+
288
+ puts "After clear (non-reserved keys removed):"
289
+ after_clear = memory.to_h
290
+ ap after_clear
291
+ puts
292
+
293
+ puts "Diff (before clear vs after clear):"
294
+ Hashdiff.diff(before_clear, after_clear).each do |change|
295
+ op, path, *values = change
296
+ case op
297
+ when "+"
298
+ puts " + #{path}: #{values.first.inspect}"
299
+ when "-"
300
+ puts " - #{path}: #{values.first.inspect}"
301
+ when "~"
302
+ puts " ~ #{path}: #{values.first.inspect} -> #{values.last.inspect}"
303
+ end
304
+ end
305
+ puts
306
+
307
+ before_reset = memory.to_h
308
+ memory.reset
309
+
310
+ puts "After reset (full reset to initial state):"
311
+ after_reset = memory.to_h
312
+ ap after_reset
313
+ puts
314
+
315
+ puts "Diff (before reset vs after reset):"
316
+ Hashdiff.diff(before_reset, after_reset).each do |change|
317
+ op, path, *values = change
318
+ case op
319
+ when "+"
320
+ puts " + #{path}: #{values.first.inspect}"
321
+ when "-"
322
+ puts " - #{path}: #{values.first.inspect}"
323
+ when "~"
324
+ puts " ~ #{path}: #{values.first.inspect} -> #{values.last.inspect}"
325
+ end
326
+ end
327
+ puts
328
+
329
+ puts "=" * 70
330
+ puts "All sections completed without any LLM calls."
331
+ puts "=" * 70