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,195 @@
1
+ # ActiveRecordAdapter
2
+
3
+ Rails ActiveRecord integration for conversation persistence.
4
+
5
+ ## Class: `RobotLab::History::ActiveRecordAdapter`
6
+
7
+ ```ruby
8
+ adapter = History::ActiveRecordAdapter.new(
9
+ thread_model: ConversationThread,
10
+ result_model: ConversationResult
11
+ )
12
+
13
+ config = adapter.to_config
14
+ ```
15
+
16
+ ## Constructor
17
+
18
+ ```ruby
19
+ ActiveRecordAdapter.new(
20
+ thread_model:,
21
+ result_model:,
22
+ thread_factory: nil,
23
+ result_factory: nil
24
+ )
25
+ ```
26
+
27
+ **Parameters:**
28
+
29
+ | Name | Type | Description |
30
+ |------|------|-------------|
31
+ | `thread_model` | `Class` | ActiveRecord model for threads |
32
+ | `result_model` | `Class` | ActiveRecord model for results |
33
+ | `thread_factory` | `Proc`, `nil` | Custom thread creation |
34
+ | `result_factory` | `Proc`, `nil` | Custom result creation |
35
+
36
+ ## Methods
37
+
38
+ ### to_config
39
+
40
+ ```ruby
41
+ config = adapter.to_config
42
+ ```
43
+
44
+ Convert to `History::Config` for use with networks.
45
+
46
+ ## Model Requirements
47
+
48
+ ### Thread Model
49
+
50
+ ```ruby
51
+ # db/migrate/xxx_create_conversation_threads.rb
52
+ create_table :conversation_threads do |t|
53
+ t.string :external_id, null: false, index: { unique: true }
54
+ t.jsonb :metadata, default: {}
55
+ t.timestamps
56
+ end
57
+
58
+ # app/models/conversation_thread.rb
59
+ class ConversationThread < ApplicationRecord
60
+ has_many :results, class_name: "ConversationResult",
61
+ foreign_key: :thread_id, dependent: :destroy
62
+ end
63
+ ```
64
+
65
+ ### Result Model
66
+
67
+ ```ruby
68
+ # db/migrate/xxx_create_conversation_results.rb
69
+ create_table :conversation_results do |t|
70
+ t.references :thread, foreign_key: { to_table: :conversation_threads }
71
+ t.string :robot_name
72
+ t.jsonb :input, default: {}
73
+ t.jsonb :output, default: []
74
+ t.jsonb :tool_calls, default: []
75
+ t.jsonb :metadata, default: {}
76
+ t.integer :position
77
+ t.timestamps
78
+ end
79
+
80
+ # app/models/conversation_result.rb
81
+ class ConversationResult < ApplicationRecord
82
+ belongs_to :thread, class_name: "ConversationThread"
83
+
84
+ def to_robot_result
85
+ RobotLab::RobotResult.from_hash(attributes)
86
+ end
87
+ end
88
+ ```
89
+
90
+ ## Examples
91
+
92
+ ### Basic Setup
93
+
94
+ ```ruby
95
+ adapter = History::ActiveRecordAdapter.new(
96
+ thread_model: ConversationThread,
97
+ result_model: ConversationResult
98
+ )
99
+
100
+ network = RobotLab.create_network do
101
+ name "chat"
102
+ history adapter.to_config
103
+ add_robot assistant
104
+ end
105
+ ```
106
+
107
+ ### With Custom Factory
108
+
109
+ ```ruby
110
+ adapter = History::ActiveRecordAdapter.new(
111
+ thread_model: ConversationThread,
112
+ result_model: ConversationResult,
113
+ thread_factory: ->(state:, input:, **context) {
114
+ ConversationThread.create!(
115
+ external_id: SecureRandom.uuid,
116
+ user_id: context[:user_id],
117
+ title: input.truncate(100),
118
+ metadata: { source: context[:source] }
119
+ )
120
+ }
121
+ )
122
+ ```
123
+
124
+ ### With User Scoping
125
+
126
+ ```ruby
127
+ class ScopedAdapter
128
+ def initialize(thread_model:, result_model:)
129
+ @thread_model = thread_model
130
+ @result_model = result_model
131
+ end
132
+
133
+ def to_config
134
+ History::Config.new(
135
+ create_thread: method(:create_thread),
136
+ get: method(:get),
137
+ append_results: method(:append_results)
138
+ )
139
+ end
140
+
141
+ private
142
+
143
+ def create_thread(state:, input:, user_id:, **)
144
+ @thread_model.create!(
145
+ external_id: SecureRandom.uuid,
146
+ user_id: user_id,
147
+ title: input.truncate(100)
148
+ )
149
+ end
150
+
151
+ def get(thread_id:, user_id:, **)
152
+ thread = @thread_model.find_by(external_id: thread_id, user_id: user_id)
153
+ return [] unless thread
154
+ thread.results.order(:position).map(&:to_robot_result)
155
+ end
156
+
157
+ def append_results(thread_id:, new_results:, user_id:, **)
158
+ thread = @thread_model.find_by!(external_id: thread_id, user_id: user_id)
159
+ position = thread.results.maximum(:position) || 0
160
+
161
+ @result_model.transaction do
162
+ new_results.each_with_index do |result, i|
163
+ thread.results.create!(
164
+ robot_name: result.robot_name,
165
+ input: result.input.to_h,
166
+ output: result.output.map(&:to_h),
167
+ tool_calls: result.tool_calls.map(&:to_h),
168
+ position: position + i + 1
169
+ )
170
+ end
171
+ end
172
+ end
173
+ end
174
+ ```
175
+
176
+ ### Rails Generator
177
+
178
+ Use the Rails generator to create models:
179
+
180
+ ```bash
181
+ rails generate robot_lab:history
182
+ ```
183
+
184
+ This creates:
185
+
186
+ - `ConversationThread` model
187
+ - `ConversationResult` model
188
+ - Database migrations
189
+ - Initializer configuration
190
+
191
+ ## See Also
192
+
193
+ - [History Overview](index.md)
194
+ - [Config](config.md)
195
+ - [Rails Integration Guide](../../guides/rails-integration.md)
@@ -0,0 +1,191 @@
1
+ # History::Config
2
+
3
+ Configuration for conversation persistence.
4
+
5
+ ## Class: `RobotLab::History::Config`
6
+
7
+ ```ruby
8
+ config = History::Config.new(
9
+ create_thread: create_proc,
10
+ get: get_proc,
11
+ append_results: append_proc
12
+ )
13
+ ```
14
+
15
+ ## Constructor
16
+
17
+ ```ruby
18
+ Config.new(
19
+ create_thread:,
20
+ get:,
21
+ append_results:
22
+ )
23
+ ```
24
+
25
+ **Parameters:**
26
+
27
+ | Name | Type | Description |
28
+ |------|------|-------------|
29
+ | `create_thread` | `Proc` | Creates a new thread |
30
+ | `get` | `Proc` | Retrieves thread history |
31
+ | `append_results` | `Proc` | Appends results to thread |
32
+
33
+ ## Callbacks
34
+
35
+ ### create_thread
36
+
37
+ Called when a new conversation starts without a thread_id.
38
+
39
+ ```ruby
40
+ create_thread: ->(state:, input:, **context) {
41
+ # Create and return thread info
42
+ { id: SecureRandom.uuid }
43
+ }
44
+ ```
45
+
46
+ **Arguments:**
47
+
48
+ | Name | Type | Description |
49
+ |------|------|-------------|
50
+ | `state` | `State` | Current state |
51
+ | `input` | `String` | User input |
52
+ | `**context` | `Hash` | Additional context |
53
+
54
+ **Returns:** Hash with `:id` key.
55
+
56
+ ### get
57
+
58
+ Called to retrieve existing conversation history.
59
+
60
+ ```ruby
61
+ get: ->(thread_id:, **context) {
62
+ # Return array of previous results
63
+ Thread.find(thread_id).results
64
+ }
65
+ ```
66
+
67
+ **Arguments:**
68
+
69
+ | Name | Type | Description |
70
+ |------|------|-------------|
71
+ | `thread_id` | `String` | Thread identifier |
72
+ | `**context` | `Hash` | Additional context |
73
+
74
+ **Returns:** Array of `RobotResult` or hashes.
75
+
76
+ ### append_results
77
+
78
+ Called after each network run to persist new results.
79
+
80
+ ```ruby
81
+ append_results: ->(thread_id:, new_results:, **context) {
82
+ # Persist the new results
83
+ thread = Thread.find(thread_id)
84
+ new_results.each { |r| thread.results.create(r.to_h) }
85
+ }
86
+ ```
87
+
88
+ **Arguments:**
89
+
90
+ | Name | Type | Description |
91
+ |------|------|-------------|
92
+ | `thread_id` | `String` | Thread identifier |
93
+ | `new_results` | `Array<RobotResult>` | Results to append |
94
+ | `**context` | `Hash` | Additional context |
95
+
96
+ ## Attributes
97
+
98
+ ### create_thread
99
+
100
+ ```ruby
101
+ config.create_thread # => Proc
102
+ ```
103
+
104
+ ### get
105
+
106
+ ```ruby
107
+ config.get # => Proc
108
+ ```
109
+
110
+ ### append_results
111
+
112
+ ```ruby
113
+ config.append_results # => Proc
114
+ ```
115
+
116
+ ## Examples
117
+
118
+ ### Basic Config
119
+
120
+ ```ruby
121
+ STORE = {}
122
+
123
+ config = History::Config.new(
124
+ create_thread: ->(state:, **) {
125
+ id = SecureRandom.uuid
126
+ STORE[id] = { results: [] }
127
+ { id: id }
128
+ },
129
+
130
+ get: ->(thread_id:, **) {
131
+ STORE.dig(thread_id, :results) || []
132
+ },
133
+
134
+ append_results: ->(thread_id:, new_results:, **) {
135
+ STORE[thread_id][:results].concat(new_results.map(&:to_h))
136
+ }
137
+ )
138
+ ```
139
+
140
+ ### With Context
141
+
142
+ ```ruby
143
+ config = History::Config.new(
144
+ create_thread: ->(state:, user_id:, **) {
145
+ Thread.create(user_id: user_id, started_at: Time.current)
146
+ },
147
+
148
+ get: ->(thread_id:, user_id:, **) {
149
+ Thread.where(id: thread_id, user_id: user_id).first&.results || []
150
+ },
151
+
152
+ append_results: ->(thread_id:, new_results:, user_id:, **) {
153
+ thread = Thread.find_by(id: thread_id, user_id: user_id)
154
+ return unless thread
155
+ new_results.each { |r| thread.results.create(r.to_h) }
156
+ }
157
+ )
158
+
159
+ # Pass context when running
160
+ network.run(state: state, user_id: current_user.id)
161
+ ```
162
+
163
+ ### With Validation
164
+
165
+ ```ruby
166
+ config = History::Config.new(
167
+ create_thread: ->(state:, **) {
168
+ raise "Invalid state" unless state.data[:user_id]
169
+ Thread.create(user_id: state.data[:user_id])
170
+ },
171
+
172
+ get: ->(thread_id:, **) {
173
+ thread = Thread.find_by(id: thread_id)
174
+ raise "Thread not found" unless thread
175
+ thread.results
176
+ },
177
+
178
+ append_results: ->(thread_id:, new_results:, **) {
179
+ thread = Thread.find(thread_id)
180
+ Thread.transaction do
181
+ new_results.each { |r| thread.results.create!(r.to_h) }
182
+ end
183
+ }
184
+ )
185
+ ```
186
+
187
+ ## See Also
188
+
189
+ - [History Overview](index.md)
190
+ - [ThreadManager](thread-manager.md)
191
+ - [ActiveRecordAdapter](active-record-adapter.md)
@@ -0,0 +1,132 @@
1
+ # History
2
+
3
+ Conversation persistence and thread management.
4
+
5
+ ## Overview
6
+
7
+ The history system enables persistent conversations by storing and retrieving conversation threads and results.
8
+
9
+ ```ruby
10
+ network = RobotLab.create_network do
11
+ name "persistent_chat"
12
+
13
+ history History::Config.new(
14
+ create_thread: ->(state:, **) { Thread.create(id: SecureRandom.uuid) },
15
+ get: ->(thread_id:, **) { Thread.find(thread_id).results },
16
+ append_results: ->(thread_id:, new_results:, **) {
17
+ Thread.find(thread_id).results.concat(new_results)
18
+ }
19
+ )
20
+
21
+ add_robot assistant
22
+ end
23
+ ```
24
+
25
+ ## Components
26
+
27
+ | Component | Description |
28
+ |-----------|-------------|
29
+ | [Config](config.md) | History configuration |
30
+ | [ThreadManager](thread-manager.md) | Thread lifecycle management |
31
+ | [ActiveRecordAdapter](active-record-adapter.md) | Rails integration |
32
+
33
+ ## Quick Start
34
+
35
+ ### Basic Configuration
36
+
37
+ ```ruby
38
+ history = History::Config.new(
39
+ create_thread: ->(state:, input:, **) {
40
+ { id: SecureRandom.uuid }
41
+ },
42
+ get: ->(thread_id:, **) {
43
+ STORE[thread_id] || []
44
+ },
45
+ append_results: ->(thread_id:, new_results:, **) {
46
+ STORE[thread_id] ||= []
47
+ STORE[thread_id].concat(new_results)
48
+ }
49
+ )
50
+ ```
51
+
52
+ ### With ActiveRecord
53
+
54
+ ```ruby
55
+ history = History::ActiveRecordAdapter.new(
56
+ thread_model: ConversationThread,
57
+ result_model: ConversationResult
58
+ ).to_config
59
+ ```
60
+
61
+ ## Callbacks
62
+
63
+ | Callback | Purpose |
64
+ |----------|---------|
65
+ | `create_thread` | Create new conversation thread |
66
+ | `get` | Retrieve existing thread history |
67
+ | `append_results` | Add results to thread |
68
+
69
+ ## Thread Lifecycle
70
+
71
+ ```mermaid
72
+ sequenceDiagram
73
+ participant U as User
74
+ participant N as Network
75
+ participant H as History
76
+
77
+ U->>N: Run with new message
78
+ N->>H: get(thread_id)
79
+ H-->>N: Previous results
80
+ N->>N: Execute robots
81
+ N->>H: append_results(new_results)
82
+ H-->>N: Saved
83
+ N-->>U: Result with thread_id
84
+ ```
85
+
86
+ ## Examples
87
+
88
+ ### In-Memory Store
89
+
90
+ ```ruby
91
+ THREADS = {}
92
+
93
+ history = History::Config.new(
94
+ create_thread: ->(state:, **) {
95
+ id = SecureRandom.uuid
96
+ THREADS[id] = []
97
+ { id: id }
98
+ },
99
+ get: ->(thread_id:, **) {
100
+ THREADS[thread_id] || []
101
+ },
102
+ append_results: ->(thread_id:, new_results:, **) {
103
+ THREADS[thread_id].concat(new_results)
104
+ }
105
+ )
106
+ ```
107
+
108
+ ### Redis Store
109
+
110
+ ```ruby
111
+ history = History::Config.new(
112
+ create_thread: ->(state:, **) {
113
+ id = SecureRandom.uuid
114
+ Redis.current.set("thread:#{id}", [].to_json)
115
+ { id: id }
116
+ },
117
+ get: ->(thread_id:, **) {
118
+ data = Redis.current.get("thread:#{thread_id}")
119
+ data ? JSON.parse(data) : []
120
+ },
121
+ append_results: ->(thread_id:, new_results:, **) {
122
+ existing = JSON.parse(Redis.current.get("thread:#{thread_id}") || "[]")
123
+ existing.concat(new_results.map(&:to_h))
124
+ Redis.current.set("thread:#{thread_id}", existing.to_json)
125
+ }
126
+ )
127
+ ```
128
+
129
+ ## See Also
130
+
131
+ - [History Guide](../../guides/history.md)
132
+ - [State](../core/state.md)
@@ -0,0 +1,144 @@
1
+ # ThreadManager
2
+
3
+ Manages conversation thread lifecycle.
4
+
5
+ ## Class: `RobotLab::History::ThreadManager`
6
+
7
+ ```ruby
8
+ manager = History::ThreadManager.new(config: history_config)
9
+ ```
10
+
11
+ ## Constructor
12
+
13
+ ```ruby
14
+ ThreadManager.new(config:)
15
+ ```
16
+
17
+ **Parameters:**
18
+
19
+ | Name | Type | Description |
20
+ |------|------|-------------|
21
+ | `config` | `Config` | History configuration |
22
+
23
+ ## Methods
24
+
25
+ ### create_thread
26
+
27
+ ```ruby
28
+ thread_info = manager.create_thread(state: state, input: input, **context)
29
+ ```
30
+
31
+ Create a new conversation thread.
32
+
33
+ **Returns:** Hash with `:id` and optional metadata.
34
+
35
+ ### get_history
36
+
37
+ ```ruby
38
+ results = manager.get_history(thread_id: id, **context)
39
+ ```
40
+
41
+ Retrieve conversation history.
42
+
43
+ **Returns:** Array of `RobotResult`.
44
+
45
+ ### append_results
46
+
47
+ ```ruby
48
+ manager.append_results(thread_id: id, new_results: results, **context)
49
+ ```
50
+
51
+ Add results to a thread.
52
+
53
+ ### ensure_thread
54
+
55
+ ```ruby
56
+ thread_id = manager.ensure_thread(state: state, input: input, **context)
57
+ ```
58
+
59
+ Create thread if state doesn't have one, or return existing.
60
+
61
+ ### load_history
62
+
63
+ ```ruby
64
+ state = manager.load_history(state: state, **context)
65
+ ```
66
+
67
+ Load history into state if thread_id exists.
68
+
69
+ ## Examples
70
+
71
+ ### Basic Usage
72
+
73
+ ```ruby
74
+ config = History::Config.new(...)
75
+ manager = History::ThreadManager.new(config: config)
76
+
77
+ # Start new conversation
78
+ state = RobotLab.create_state(message: "Hello")
79
+ thread_id = manager.ensure_thread(state: state, input: "Hello")
80
+ state.thread_id = thread_id
81
+
82
+ # Run and save
83
+ result = network.run(state: state)
84
+ manager.append_results(thread_id: thread_id, new_results: result.new_results)
85
+
86
+ # Continue conversation
87
+ state2 = RobotLab.create_state(message: "Follow up")
88
+ state2.thread_id = thread_id
89
+ state2 = manager.load_history(state: state2)
90
+ # state2 now has previous results
91
+ ```
92
+
93
+ ### In Network
94
+
95
+ ```ruby
96
+ # ThreadManager is used internally by Network
97
+ network = RobotLab.create_network do
98
+ history config
99
+ add_robot assistant
100
+ end
101
+
102
+ # First message - thread created automatically
103
+ result1 = network.run(state: state1)
104
+ thread_id = result1.state.thread_id
105
+
106
+ # Continue - history loaded automatically
107
+ state2 = RobotLab.create_state(
108
+ message: UserMessage.new("Continue", thread_id: thread_id)
109
+ )
110
+ result2 = network.run(state: state2)
111
+ ```
112
+
113
+ ### Custom Thread Data
114
+
115
+ ```ruby
116
+ manager = History::ThreadManager.new(
117
+ config: History::Config.new(
118
+ create_thread: ->(state:, input:, metadata:, **) {
119
+ Thread.create(
120
+ title: input.truncate(50),
121
+ metadata: metadata,
122
+ created_at: Time.current
123
+ )
124
+ },
125
+ get: ->(thread_id:, **) { Thread.find(thread_id).results },
126
+ append_results: ->(thread_id:, new_results:, **) {
127
+ Thread.find(thread_id).results.concat(new_results)
128
+ }
129
+ )
130
+ )
131
+
132
+ # Pass metadata when creating thread
133
+ thread = manager.create_thread(
134
+ state: state,
135
+ input: "Help with billing",
136
+ metadata: { source: "web", priority: "high" }
137
+ )
138
+ ```
139
+
140
+ ## See Also
141
+
142
+ - [History Overview](index.md)
143
+ - [Config](config.md)
144
+ - [ActiveRecordAdapter](active-record-adapter.md)