robot_lab 0.0.1

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 (153) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +1 -0
  3. data/.github/workflows/deploy-github-pages.yml +52 -0
  4. data/.github/workflows/deploy-yard-docs.yml +52 -0
  5. data/CHANGELOG.md +55 -0
  6. data/COMMITS.md +196 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +332 -0
  9. data/Rakefile +67 -0
  10. data/docs/api/adapters/anthropic.md +121 -0
  11. data/docs/api/adapters/gemini.md +133 -0
  12. data/docs/api/adapters/index.md +104 -0
  13. data/docs/api/adapters/openai.md +134 -0
  14. data/docs/api/core/index.md +113 -0
  15. data/docs/api/core/memory.md +314 -0
  16. data/docs/api/core/network.md +291 -0
  17. data/docs/api/core/robot.md +273 -0
  18. data/docs/api/core/state.md +273 -0
  19. data/docs/api/core/tool.md +353 -0
  20. data/docs/api/history/active-record-adapter.md +195 -0
  21. data/docs/api/history/config.md +191 -0
  22. data/docs/api/history/index.md +132 -0
  23. data/docs/api/history/thread-manager.md +144 -0
  24. data/docs/api/index.md +82 -0
  25. data/docs/api/mcp/client.md +221 -0
  26. data/docs/api/mcp/index.md +111 -0
  27. data/docs/api/mcp/server.md +225 -0
  28. data/docs/api/mcp/transports.md +264 -0
  29. data/docs/api/messages/index.md +67 -0
  30. data/docs/api/messages/text-message.md +102 -0
  31. data/docs/api/messages/tool-call-message.md +144 -0
  32. data/docs/api/messages/tool-result-message.md +154 -0
  33. data/docs/api/messages/user-message.md +171 -0
  34. data/docs/api/streaming/context.md +174 -0
  35. data/docs/api/streaming/events.md +237 -0
  36. data/docs/api/streaming/index.md +108 -0
  37. data/docs/architecture/core-concepts.md +243 -0
  38. data/docs/architecture/index.md +138 -0
  39. data/docs/architecture/message-flow.md +320 -0
  40. data/docs/architecture/network-orchestration.md +216 -0
  41. data/docs/architecture/robot-execution.md +243 -0
  42. data/docs/architecture/state-management.md +323 -0
  43. data/docs/assets/css/custom.css +56 -0
  44. data/docs/assets/images/robot_lab.jpg +0 -0
  45. data/docs/concepts.md +216 -0
  46. data/docs/examples/basic-chat.md +193 -0
  47. data/docs/examples/index.md +129 -0
  48. data/docs/examples/mcp-server.md +290 -0
  49. data/docs/examples/multi-robot-network.md +312 -0
  50. data/docs/examples/rails-application.md +420 -0
  51. data/docs/examples/tool-usage.md +310 -0
  52. data/docs/getting-started/configuration.md +230 -0
  53. data/docs/getting-started/index.md +56 -0
  54. data/docs/getting-started/installation.md +179 -0
  55. data/docs/getting-started/quick-start.md +203 -0
  56. data/docs/guides/building-robots.md +376 -0
  57. data/docs/guides/creating-networks.md +366 -0
  58. data/docs/guides/history.md +359 -0
  59. data/docs/guides/index.md +68 -0
  60. data/docs/guides/mcp-integration.md +356 -0
  61. data/docs/guides/memory.md +309 -0
  62. data/docs/guides/rails-integration.md +432 -0
  63. data/docs/guides/streaming.md +314 -0
  64. data/docs/guides/using-tools.md +394 -0
  65. data/docs/index.md +160 -0
  66. data/examples/01_simple_robot.rb +38 -0
  67. data/examples/02_tools.rb +106 -0
  68. data/examples/03_network.rb +103 -0
  69. data/examples/04_mcp.rb +219 -0
  70. data/examples/05_streaming.rb +124 -0
  71. data/examples/06_prompt_templates.rb +324 -0
  72. data/examples/07_network_memory.rb +329 -0
  73. data/examples/prompts/assistant/system.txt.erb +2 -0
  74. data/examples/prompts/assistant/user.txt.erb +1 -0
  75. data/examples/prompts/billing/system.txt.erb +7 -0
  76. data/examples/prompts/billing/user.txt.erb +1 -0
  77. data/examples/prompts/classifier/system.txt.erb +4 -0
  78. data/examples/prompts/classifier/user.txt.erb +1 -0
  79. data/examples/prompts/entity_extractor/system.txt.erb +11 -0
  80. data/examples/prompts/entity_extractor/user.txt.erb +3 -0
  81. data/examples/prompts/escalation/system.txt.erb +35 -0
  82. data/examples/prompts/escalation/user.txt.erb +34 -0
  83. data/examples/prompts/general/system.txt.erb +4 -0
  84. data/examples/prompts/general/user.txt.erb +1 -0
  85. data/examples/prompts/github_assistant/system.txt.erb +6 -0
  86. data/examples/prompts/github_assistant/user.txt.erb +1 -0
  87. data/examples/prompts/helper/system.txt.erb +1 -0
  88. data/examples/prompts/helper/user.txt.erb +1 -0
  89. data/examples/prompts/keyword_extractor/system.txt.erb +8 -0
  90. data/examples/prompts/keyword_extractor/user.txt.erb +3 -0
  91. data/examples/prompts/order_support/system.txt.erb +27 -0
  92. data/examples/prompts/order_support/user.txt.erb +22 -0
  93. data/examples/prompts/product_support/system.txt.erb +30 -0
  94. data/examples/prompts/product_support/user.txt.erb +32 -0
  95. data/examples/prompts/sentiment_analyzer/system.txt.erb +9 -0
  96. data/examples/prompts/sentiment_analyzer/user.txt.erb +3 -0
  97. data/examples/prompts/synthesizer/system.txt.erb +14 -0
  98. data/examples/prompts/synthesizer/user.txt.erb +15 -0
  99. data/examples/prompts/technical/system.txt.erb +7 -0
  100. data/examples/prompts/technical/user.txt.erb +1 -0
  101. data/examples/prompts/triage/system.txt.erb +16 -0
  102. data/examples/prompts/triage/user.txt.erb +17 -0
  103. data/lib/generators/robot_lab/install_generator.rb +78 -0
  104. data/lib/generators/robot_lab/robot_generator.rb +55 -0
  105. data/lib/generators/robot_lab/templates/initializer.rb.tt +41 -0
  106. data/lib/generators/robot_lab/templates/migration.rb.tt +32 -0
  107. data/lib/generators/robot_lab/templates/result_model.rb.tt +52 -0
  108. data/lib/generators/robot_lab/templates/robot.rb.tt +46 -0
  109. data/lib/generators/robot_lab/templates/robot_test.rb.tt +32 -0
  110. data/lib/generators/robot_lab/templates/routing_robot.rb.tt +53 -0
  111. data/lib/generators/robot_lab/templates/thread_model.rb.tt +40 -0
  112. data/lib/robot_lab/adapters/anthropic.rb +163 -0
  113. data/lib/robot_lab/adapters/base.rb +85 -0
  114. data/lib/robot_lab/adapters/gemini.rb +193 -0
  115. data/lib/robot_lab/adapters/openai.rb +159 -0
  116. data/lib/robot_lab/adapters/registry.rb +81 -0
  117. data/lib/robot_lab/configuration.rb +143 -0
  118. data/lib/robot_lab/error.rb +32 -0
  119. data/lib/robot_lab/errors.rb +70 -0
  120. data/lib/robot_lab/history/active_record_adapter.rb +146 -0
  121. data/lib/robot_lab/history/config.rb +115 -0
  122. data/lib/robot_lab/history/thread_manager.rb +93 -0
  123. data/lib/robot_lab/mcp/client.rb +210 -0
  124. data/lib/robot_lab/mcp/server.rb +84 -0
  125. data/lib/robot_lab/mcp/transports/base.rb +56 -0
  126. data/lib/robot_lab/mcp/transports/sse.rb +117 -0
  127. data/lib/robot_lab/mcp/transports/stdio.rb +133 -0
  128. data/lib/robot_lab/mcp/transports/streamable_http.rb +139 -0
  129. data/lib/robot_lab/mcp/transports/websocket.rb +108 -0
  130. data/lib/robot_lab/memory.rb +882 -0
  131. data/lib/robot_lab/memory_change.rb +123 -0
  132. data/lib/robot_lab/message.rb +357 -0
  133. data/lib/robot_lab/network.rb +350 -0
  134. data/lib/robot_lab/rails/engine.rb +29 -0
  135. data/lib/robot_lab/rails/railtie.rb +42 -0
  136. data/lib/robot_lab/robot.rb +560 -0
  137. data/lib/robot_lab/robot_result.rb +205 -0
  138. data/lib/robot_lab/robotic_model.rb +324 -0
  139. data/lib/robot_lab/state_proxy.rb +188 -0
  140. data/lib/robot_lab/streaming/context.rb +144 -0
  141. data/lib/robot_lab/streaming/events.rb +95 -0
  142. data/lib/robot_lab/streaming/sequence_counter.rb +48 -0
  143. data/lib/robot_lab/task.rb +117 -0
  144. data/lib/robot_lab/tool.rb +223 -0
  145. data/lib/robot_lab/tool_config.rb +112 -0
  146. data/lib/robot_lab/tool_manifest.rb +234 -0
  147. data/lib/robot_lab/user_message.rb +118 -0
  148. data/lib/robot_lab/version.rb +5 -0
  149. data/lib/robot_lab/waiter.rb +73 -0
  150. data/lib/robot_lab.rb +195 -0
  151. data/mkdocs.yml +214 -0
  152. data/sig/robot_lab.rbs +4 -0
  153. metadata +442 -0
@@ -0,0 +1,366 @@
1
+ # Creating Networks
2
+
3
+ Networks orchestrate multiple robots using [SimpleFlow](https://github.com/MadBomber/simple_flow) pipelines with DAG-based execution and optional task activation.
4
+
5
+ ## Basic Network
6
+
7
+ Create a network with a sequential pipeline:
8
+
9
+ ```ruby
10
+ network = RobotLab.create_network(name: "pipeline") do
11
+ task :analyzer, analyzer_robot, depends_on: :none
12
+ task :writer, writer_robot, depends_on: [:analyzer]
13
+ task :reviewer, reviewer_robot, depends_on: [:writer]
14
+ end
15
+
16
+ result = network.run(message: "Analyze this document")
17
+ ```
18
+
19
+ ## Network Properties
20
+
21
+ ### Name
22
+
23
+ Identifies the network for logging and debugging:
24
+
25
+ ```ruby
26
+ network = RobotLab.create_network(name: "customer_service") do
27
+ # ...
28
+ end
29
+ ```
30
+
31
+ ### Concurrency
32
+
33
+ Control parallel execution mode:
34
+
35
+ ```ruby
36
+ network = RobotLab.create_network(name: "parallel", concurrency: :threads) do
37
+ # :auto (default), :threads, or :async
38
+ end
39
+ ```
40
+
41
+ ## Adding Tasks
42
+
43
+ ### Sequential Tasks
44
+
45
+ Each task depends on the previous:
46
+
47
+ ```ruby
48
+ network = RobotLab.create_network(name: "pipeline") do
49
+ task :first, robot1, depends_on: :none
50
+ task :second, robot2, depends_on: [:first]
51
+ task :third, robot3, depends_on: [:second]
52
+ end
53
+ ```
54
+
55
+ ### Parallel Tasks
56
+
57
+ Tasks with the same dependencies run in parallel:
58
+
59
+ ```ruby
60
+ network = RobotLab.create_network(name: "parallel_analysis") do
61
+ task :fetch, fetcher, depends_on: :none
62
+
63
+ # These run in parallel after :fetch
64
+ task :sentiment, sentiment_bot, depends_on: [:fetch]
65
+ task :entities, entity_bot, depends_on: [:fetch]
66
+ task :keywords, keyword_bot, depends_on: [:fetch]
67
+
68
+ # This waits for all three to complete
69
+ task :merge, merger, depends_on: [:sentiment, :entities, :keywords]
70
+ end
71
+ ```
72
+
73
+ ### Optional Tasks
74
+
75
+ Optional tasks only run when explicitly activated:
76
+
77
+ ```ruby
78
+ network = RobotLab.create_network(name: "router") do
79
+ task :classifier, classifier_robot, depends_on: :none
80
+ task :billing, billing_robot, depends_on: :optional
81
+ task :technical, technical_robot, depends_on: :optional
82
+ task :general, general_robot, depends_on: :optional
83
+ end
84
+ ```
85
+
86
+ ## Per-Task Configuration
87
+
88
+ Tasks can have individual context and configuration that's deep-merged with the network's run parameters:
89
+
90
+ ```ruby
91
+ network = RobotLab.create_network(name: "support") do
92
+ task :classifier, classifier_robot, depends_on: :none
93
+ task :billing, billing_robot,
94
+ context: { department: "billing", escalation_level: 2 },
95
+ depends_on: :optional
96
+ task :technical, technical_robot,
97
+ context: { department: "technical" },
98
+ tools: [DebugTool, LogTool],
99
+ depends_on: :optional
100
+ end
101
+ ```
102
+
103
+ ### Task Options
104
+
105
+ | Option | Description |
106
+ |--------|-------------|
107
+ | `context` | Hash merged with run params (task values override) |
108
+ | `mcp` | MCP servers for this task |
109
+ | `tools` | Tools available to this task |
110
+ | `memory` | Task-specific memory |
111
+ | `depends_on` | `:none`, `[:task1]`, or `:optional` |
112
+
113
+ ## Conditional Routing
114
+
115
+ Use optional tasks with custom Robot subclasses for intelligent routing:
116
+
117
+ ```ruby
118
+ class ClassifierRobot < RobotLab::Robot
119
+ def call(result)
120
+ robot_result = run(**extract_run_context(result))
121
+
122
+ new_result = result
123
+ .with_context(@name.to_sym, robot_result)
124
+ .continue(robot_result)
125
+
126
+ # Activate appropriate specialist based on classification
127
+ category = robot_result.last_text_content.to_s.strip.downcase
128
+
129
+ case category
130
+ when /billing/ then new_result.activate(:billing)
131
+ when /technical/ then new_result.activate(:technical)
132
+ else new_result.activate(:general)
133
+ end
134
+ end
135
+ end
136
+
137
+ classifier = ClassifierRobot.new(
138
+ name: "classifier",
139
+ system_prompt: "Classify as: billing, technical, or general. Respond with one word."
140
+ )
141
+
142
+ network = RobotLab.create_network(name: "support") do
143
+ task :classifier, classifier, depends_on: :none
144
+ task :billing, billing_robot, depends_on: :optional
145
+ task :technical, technical_robot, depends_on: :optional
146
+ task :general, general_robot, depends_on: :optional
147
+ end
148
+ ```
149
+
150
+ ## Running Networks
151
+
152
+ ### Basic Run
153
+
154
+ ```ruby
155
+ result = network.run(message: "Help me with my order")
156
+
157
+ # Get the final response
158
+ puts result.value.last_text_content
159
+ ```
160
+
161
+ ### With Additional Context
162
+
163
+ ```ruby
164
+ result = network.run(
165
+ message: "Check my order status",
166
+ customer_id: 123,
167
+ order_id: "ORD-456"
168
+ )
169
+ ```
170
+
171
+ ### Accessing Task Results
172
+
173
+ ```ruby
174
+ result = network.run(message: "Process this")
175
+
176
+ # Access individual robot results
177
+ classifier_result = result.context[:classifier]
178
+ billing_result = result.context[:billing]
179
+
180
+ # Original run parameters
181
+ original_params = result.context[:run_params]
182
+ ```
183
+
184
+ ## SimpleFlow::Result
185
+
186
+ Networks return a `SimpleFlow::Result` object:
187
+
188
+ ```ruby
189
+ result = network.run(message: "Hello")
190
+
191
+ result.value # The final task's output (RobotResult)
192
+ result.context # Hash of all task results and metadata
193
+ result.halted? # Whether execution was halted early
194
+ result.continued? # Whether execution continued normally
195
+ ```
196
+
197
+ ## Patterns
198
+
199
+ ### Classifier Pattern
200
+
201
+ Route to specialists based on classification:
202
+
203
+ ```ruby
204
+ class SupportClassifier < RobotLab::Robot
205
+ def call(result)
206
+ robot_result = run(**extract_run_context(result))
207
+ new_result = result
208
+ .with_context(@name.to_sym, robot_result)
209
+ .continue(robot_result)
210
+
211
+ category = robot_result.last_text_content.to_s.strip.downcase
212
+ new_result.activate(category.to_sym)
213
+ end
214
+ end
215
+
216
+ network = RobotLab.create_network(name: "support") do
217
+ task :classifier, SupportClassifier.new(name: "classifier", template: :classifier),
218
+ depends_on: :none
219
+ task :billing, billing_robot, depends_on: :optional
220
+ task :technical, technical_robot, depends_on: :optional
221
+ task :general, general_robot, depends_on: :optional
222
+ end
223
+ ```
224
+
225
+ ### Pipeline Pattern
226
+
227
+ Process through sequential stages:
228
+
229
+ ```ruby
230
+ network = RobotLab.create_network(name: "document_processor") do
231
+ task :extract, extractor, depends_on: :none
232
+ task :analyze, analyzer, depends_on: [:extract]
233
+ task :format, formatter, depends_on: [:analyze]
234
+ end
235
+ ```
236
+
237
+ ### Fan-Out/Fan-In Pattern
238
+
239
+ Parallel processing with aggregation:
240
+
241
+ ```ruby
242
+ network = RobotLab.create_network(name: "multi_analysis") do
243
+ task :prepare, preparer, depends_on: :none
244
+
245
+ # Fan-out: parallel analysis
246
+ task :sentiment, sentiment_analyzer, depends_on: [:prepare]
247
+ task :topics, topic_extractor, depends_on: [:prepare]
248
+ task :entities, entity_recognizer, depends_on: [:prepare]
249
+
250
+ # Fan-in: aggregate results
251
+ task :aggregate, aggregator, depends_on: [:sentiment, :topics, :entities]
252
+ end
253
+ ```
254
+
255
+ ### Conditional Continuation
256
+
257
+ A robot can halt execution early:
258
+
259
+ ```ruby
260
+ class ValidatorRobot < RobotLab::Robot
261
+ def call(result)
262
+ robot_result = run(**extract_run_context(result))
263
+
264
+ if robot_result.last_text_content.include?("INVALID")
265
+ # Stop the pipeline
266
+ result.halt(robot_result)
267
+ else
268
+ # Continue to next task
269
+ result
270
+ .with_context(@name.to_sym, robot_result)
271
+ .continue(robot_result)
272
+ end
273
+ end
274
+ end
275
+ ```
276
+
277
+ ## Visualization
278
+
279
+ ### ASCII Visualization
280
+
281
+ ```ruby
282
+ puts network.visualize
283
+ # => ASCII representation of the pipeline
284
+ ```
285
+
286
+ ### Mermaid Diagram
287
+
288
+ ```ruby
289
+ puts network.to_mermaid
290
+ # => Mermaid graph definition
291
+ ```
292
+
293
+ ### Execution Plan
294
+
295
+ ```ruby
296
+ puts network.execution_plan
297
+ # => Description of execution order
298
+ ```
299
+
300
+ ## Network Introspection
301
+
302
+ ```ruby
303
+ network.name # => "support"
304
+ network.robots # => Hash of name => Robot
305
+ network.robot(:billing) # => Robot instance
306
+ network["billing"] # => Robot instance (alias)
307
+ network.available_robots # => Array of Robot instances
308
+ network.to_h # => Hash representation
309
+ ```
310
+
311
+ ## Best Practices
312
+
313
+ ### 1. Keep Robots Focused
314
+
315
+ Each robot should have a single responsibility:
316
+
317
+ ```ruby
318
+ # Good: focused robots
319
+ task :classify, classifier, depends_on: :none
320
+ task :respond, responder, depends_on: [:classify]
321
+
322
+ # Bad: one robot doing everything
323
+ task :do_everything, mega_robot, depends_on: :none
324
+ ```
325
+
326
+ ### 2. Use Context for Data Passing
327
+
328
+ Access previous results via context:
329
+
330
+ ```ruby
331
+ class ResponderRobot < RobotLab::Robot
332
+ def call(result)
333
+ # Get classifier's output
334
+ classification = result.context[:classifier]&.last_text_content
335
+
336
+ # Use it in this robot's run
337
+ robot_result = run(
338
+ **extract_run_context(result),
339
+ classification: classification
340
+ )
341
+
342
+ result.with_context(@name.to_sym, robot_result).continue(robot_result)
343
+ end
344
+ end
345
+ ```
346
+
347
+ ### 3. Handle Missing Results
348
+
349
+ Guard against missing optional task results:
350
+
351
+ ```ruby
352
+ def call(result)
353
+ # Check if optional task ran
354
+ if result.context[:validator]
355
+ # Use validator result
356
+ else
357
+ # Handle missing validation
358
+ end
359
+ end
360
+ ```
361
+
362
+ ## Next Steps
363
+
364
+ - [Using Tools](using-tools.md) - Add capabilities to robots
365
+ - [Memory Guide](memory.md) - Persistent memory across runs
366
+ - [API Reference: Network](../api/core/network.md) - Complete API
@@ -0,0 +1,359 @@
1
+ # Conversation History
2
+
3
+ Persist and restore conversation threads across sessions.
4
+
5
+ ## Overview
6
+
7
+ History allows you to:
8
+
9
+ - Save conversation results to a database
10
+ - Restore previous conversations
11
+ - Continue multi-turn interactions
12
+ - Maintain context across sessions
13
+
14
+ ## Configuration
15
+
16
+ ### History Config
17
+
18
+ Configure history with callbacks:
19
+
20
+ ```ruby
21
+ history_config = RobotLab::History::Config.new(
22
+ create_thread: ->(state:, input:, **) {
23
+ # Create a new thread, return thread_id
24
+ { thread_id: SecureRandom.uuid }
25
+ },
26
+
27
+ get: ->(thread_id:, **) {
28
+ # Retrieve history for thread
29
+ # Return Array<RobotResult>
30
+ []
31
+ },
32
+
33
+ append_user_message: ->(thread_id:, message:, **) {
34
+ # Optional: Store user message
35
+ },
36
+
37
+ append_results: ->(thread_id:, new_results:, **) {
38
+ # Store new results
39
+ }
40
+ )
41
+ ```
42
+
43
+ ### Apply to Network
44
+
45
+ ```ruby
46
+ network = RobotLab.create_network do
47
+ name "persistent_chat"
48
+ history history_config
49
+ end
50
+ ```
51
+
52
+ ## Callback Reference
53
+
54
+ ### create_thread
55
+
56
+ Called when starting a new conversation:
57
+
58
+ ```ruby
59
+ create_thread: ->(state:, input:, **kwargs) {
60
+ # state - Current State object
61
+ # input - UserMessage or string
62
+ # kwargs - Additional context
63
+
64
+ thread = Thread.create!(
65
+ initial_input: input.to_s,
66
+ user_id: state.data[:user_id]
67
+ )
68
+
69
+ { thread_id: thread.id.to_s } # Must return hash with :thread_id
70
+ }
71
+ ```
72
+
73
+ ### get
74
+
75
+ Called to retrieve existing history:
76
+
77
+ ```ruby
78
+ get: ->(thread_id:, **kwargs) {
79
+ # thread_id - The thread identifier
80
+ # kwargs - Additional context
81
+
82
+ Result.where(thread_id: thread_id)
83
+ .order(:created_at)
84
+ .map { |r| deserialize_result(r) }
85
+
86
+ # Must return Array<RobotResult>
87
+ }
88
+ ```
89
+
90
+ ### append_user_message (Optional)
91
+
92
+ Called when a user message is added:
93
+
94
+ ```ruby
95
+ append_user_message: ->(thread_id:, message:, **kwargs) {
96
+ # thread_id - The thread identifier
97
+ # message - UserMessage object
98
+
99
+ Message.create!(
100
+ thread_id: thread_id,
101
+ content: message.content,
102
+ metadata: message.metadata
103
+ )
104
+ }
105
+ ```
106
+
107
+ ### append_results
108
+
109
+ Called after robots finish:
110
+
111
+ ```ruby
112
+ append_results: ->(thread_id:, new_results:, **kwargs) {
113
+ # thread_id - The thread identifier
114
+ # new_results - Array<RobotResult>
115
+
116
+ new_results.each do |result|
117
+ Result.create!(
118
+ thread_id: thread_id,
119
+ robot_name: result.robot_name,
120
+ output_data: serialize_output(result.output),
121
+ stop_reason: result.stop_reason
122
+ )
123
+ end
124
+ }
125
+ ```
126
+
127
+ ## ActiveRecord Adapter
128
+
129
+ RobotLab includes a built-in ActiveRecord adapter:
130
+
131
+ ```ruby
132
+ adapter = RobotLab::History::ActiveRecordAdapter.new(
133
+ thread_model: RobotLabThread,
134
+ result_model: RobotLabResult
135
+ )
136
+
137
+ network = RobotLab.create_network do
138
+ history adapter.to_config
139
+ end
140
+ ```
141
+
142
+ ### Required Models
143
+
144
+ ```ruby title="app/models/robot_lab_thread.rb"
145
+ class RobotLabThread < ApplicationRecord
146
+ has_many :results, class_name: "RobotLabResult", foreign_key: :thread_id
147
+
148
+ # Required columns:
149
+ # - thread_id: string
150
+ # - initial_input: text
151
+ # - input_metadata: jsonb
152
+ # - state_data: jsonb
153
+ # - last_user_message: text
154
+ # - last_user_message_at: datetime
155
+ end
156
+ ```
157
+
158
+ ```ruby title="app/models/robot_lab_result.rb"
159
+ class RobotLabResult < ApplicationRecord
160
+ belongs_to :thread, class_name: "RobotLabThread", foreign_key: :thread_id
161
+
162
+ # Required columns:
163
+ # - thread_id: string
164
+ # - robot_name: string
165
+ # - sequence_number: integer
166
+ # - output_data: jsonb
167
+ # - tool_calls_data: jsonb
168
+ # - stop_reason: string
169
+ # - checksum: string
170
+ end
171
+ ```
172
+
173
+ ## Using Thread IDs
174
+
175
+ ### Start New Thread
176
+
177
+ ```ruby
178
+ state = RobotLab.create_state(message: "Hello!")
179
+ result = network.run(state: state)
180
+
181
+ # Thread ID is assigned automatically
182
+ thread_id = state.thread_id
183
+ ```
184
+
185
+ ### Continue Existing Thread
186
+
187
+ ```ruby
188
+ # Option 1: Via UserMessage
189
+ message = RobotLab::UserMessage.new(
190
+ "Continue our conversation",
191
+ thread_id: existing_thread_id
192
+ )
193
+ state = RobotLab.create_state(message: message)
194
+
195
+ # Option 2: Direct assignment
196
+ state = RobotLab.create_state(message: "Continue")
197
+ state.thread_id = existing_thread_id
198
+
199
+ # History is automatically loaded
200
+ result = network.run(state: state)
201
+ ```
202
+
203
+ ## ThreadManager
204
+
205
+ For programmatic control:
206
+
207
+ ```ruby
208
+ manager = RobotLab::History::ThreadManager.new(history_config)
209
+
210
+ # Create thread
211
+ thread_id = manager.create_thread(state: state, input: message)
212
+
213
+ # Load history
214
+ results = manager.get_history(thread_id)
215
+
216
+ # Save state
217
+ manager.save_state(thread_id: thread_id, state: state, since_index: 5)
218
+ ```
219
+
220
+ ## Serialization
221
+
222
+ ### RobotResult
223
+
224
+ Results are serialized via `export`:
225
+
226
+ ```ruby
227
+ result.export
228
+ # => {
229
+ # robot_name: "assistant",
230
+ # output: [...],
231
+ # tool_calls: [...],
232
+ # stop_reason: "stop",
233
+ # id: "...",
234
+ # created_at: "..."
235
+ # }
236
+ ```
237
+
238
+ ### Messages
239
+
240
+ Messages serialize to hashes:
241
+
242
+ ```ruby
243
+ message.to_h
244
+ # => {
245
+ # type: "text",
246
+ # role: "assistant",
247
+ # content: "Hello!",
248
+ # stop_reason: "stop"
249
+ # }
250
+ ```
251
+
252
+ ### Restore from hash
253
+
254
+ ```ruby
255
+ RobotLab::Message.from_hash(hash)
256
+ ```
257
+
258
+ ## Patterns
259
+
260
+ ### Redis-Based History
261
+
262
+ ```ruby
263
+ history_config = History::Config.new(
264
+ create_thread: ->(state:, input:, **) {
265
+ thread_id = SecureRandom.uuid
266
+ Redis.current.hset("threads", thread_id, input.to_s)
267
+ { thread_id: thread_id }
268
+ },
269
+
270
+ get: ->(thread_id:, **) {
271
+ data = Redis.current.lrange("results:#{thread_id}", 0, -1)
272
+ data.map { |json| deserialize_result(JSON.parse(json)) }
273
+ },
274
+
275
+ append_results: ->(thread_id:, new_results:, **) {
276
+ new_results.each do |result|
277
+ Redis.current.rpush("results:#{thread_id}", result.export.to_json)
278
+ end
279
+ }
280
+ )
281
+ ```
282
+
283
+ ### Custom Storage
284
+
285
+ ```ruby
286
+ class CustomHistoryAdapter
287
+ def initialize(storage)
288
+ @storage = storage
289
+ end
290
+
291
+ def to_config
292
+ History::Config.new(
293
+ create_thread: method(:create_thread),
294
+ get: method(:get),
295
+ append_results: method(:append_results)
296
+ )
297
+ end
298
+
299
+ private
300
+
301
+ def create_thread(state:, input:, **)
302
+ id = @storage.create_conversation(input: input.to_s)
303
+ { thread_id: id }
304
+ end
305
+
306
+ def get(thread_id:, **)
307
+ @storage.fetch_results(thread_id)
308
+ end
309
+
310
+ def append_results(thread_id:, new_results:, **)
311
+ @storage.store_results(thread_id, new_results)
312
+ end
313
+ end
314
+ ```
315
+
316
+ ## Best Practices
317
+
318
+ ### 1. Handle Missing Threads
319
+
320
+ ```ruby
321
+ get: ->(thread_id:, **) {
322
+ thread = Thread.find_by(thread_id: thread_id)
323
+ return [] unless thread
324
+
325
+ thread.results.order(:created_at).map(&:to_robot_result)
326
+ }
327
+ ```
328
+
329
+ ### 2. Index for Performance
330
+
331
+ ```sql
332
+ CREATE INDEX idx_results_thread_id ON robot_lab_results(thread_id);
333
+ CREATE INDEX idx_results_created_at ON robot_lab_results(created_at);
334
+ ```
335
+
336
+ ### 3. Clean Up Old Threads
337
+
338
+ ```ruby
339
+ # Periodic cleanup job
340
+ Thread.where("updated_at < ?", 30.days.ago).destroy_all
341
+ ```
342
+
343
+ ### 4. Limit History Size
344
+
345
+ ```ruby
346
+ get: ->(thread_id:, **) {
347
+ Result.where(thread_id: thread_id)
348
+ .order(created_at: :desc)
349
+ .limit(50) # Last 50 exchanges
350
+ .reverse
351
+ .map(&:to_robot_result)
352
+ }
353
+ ```
354
+
355
+ ## Next Steps
356
+
357
+ - [Memory System](memory.md) - In-memory data sharing
358
+ - [State Management](../architecture/state-management.md) - State details
359
+ - [API Reference: History](../api/history/index.md) - Complete API