robot_lab 0.2.6 → 0.2.7

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 (109) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +124 -64
  3. data/docs/api/core/index.md +41 -15
  4. data/docs/api/core/memory.md +247 -29
  5. data/docs/api/core/network.md +255 -33
  6. data/docs/api/core/result.md +120 -32
  7. data/docs/api/core/robot.md +551 -61
  8. data/docs/api/core/state.md +87 -197
  9. data/docs/api/core/tool.md +165 -20
  10. data/docs/api/errors.md +110 -17
  11. data/docs/api/hooks.md +469 -0
  12. data/docs/api/index.md +80 -7
  13. data/docs/api/mcp/client.md +129 -35
  14. data/docs/api/mcp/index.md +164 -23
  15. data/docs/api/mcp/server.md +27 -3
  16. data/docs/api/mcp/transports.md +94 -22
  17. data/docs/api/messages/index.md +26 -3
  18. data/docs/api/messages/text-message.md +33 -11
  19. data/docs/api/messages/tool-call-message.md +27 -4
  20. data/docs/api/messages/tool-result-message.md +23 -4
  21. data/docs/api/messages/user-message.md +45 -8
  22. data/docs/api/skills.md +519 -0
  23. data/docs/api/streaming/context.md +28 -5
  24. data/docs/api/streaming/index.md +57 -11
  25. data/docs/api/support.md +846 -0
  26. data/docs/architecture/core-concepts.md +79 -31
  27. data/docs/architecture/index.md +86 -11
  28. data/docs/architecture/message-flow.md +66 -29
  29. data/docs/architecture/network-orchestration.md +145 -38
  30. data/docs/architecture/robot-execution.md +172 -90
  31. data/docs/architecture/state-management.md +31 -12
  32. data/docs/concepts.md +176 -21
  33. data/docs/examples/basic-chat.md +72 -19
  34. data/docs/examples/index.md +117 -31
  35. data/docs/examples/mcp-server.md +154 -45
  36. data/docs/examples/multi-robot-network.md +91 -21
  37. data/docs/examples/tool-usage.md +104 -37
  38. data/docs/getting-started/configuration.md +284 -97
  39. data/docs/getting-started/installation.md +53 -41
  40. data/docs/getting-started/quick-start.md +51 -6
  41. data/docs/guides/building-robots.md +258 -50
  42. data/docs/guides/creating-networks.md +214 -30
  43. data/docs/guides/hooks.md +141 -54
  44. data/docs/guides/knowledge.md +35 -4
  45. data/docs/guides/mcp-integration.md +211 -44
  46. data/docs/guides/memory.md +103 -12
  47. data/docs/guides/observability.md +95 -47
  48. data/docs/guides/streaming.md +184 -125
  49. data/docs/guides/using-tools.md +237 -17
  50. data/docs/index.md +36 -4
  51. data/examples/01_simple_robot.rb +2 -2
  52. data/examples/02_tools.rb +14 -4
  53. data/examples/03_network.rb +12 -7
  54. data/examples/04_mcp.rb +11 -4
  55. data/examples/05_streaming.rb +8 -5
  56. data/examples/06_prompt_templates.rb +13 -9
  57. data/examples/07_network_memory.rb +5 -5
  58. data/examples/08_llm_config.rb +20 -15
  59. data/examples/09_chaining.rb +4 -4
  60. data/examples/11_network_introspection.rb +4 -4
  61. data/examples/12_message_bus.rb +2 -2
  62. data/examples/13_spawn.rb +2 -2
  63. data/examples/14_rusty_circuit/README.md +1 -0
  64. data/examples/14_rusty_circuit/comic.rb +7 -3
  65. data/examples/14_rusty_circuit/display.rb +14 -0
  66. data/examples/14_rusty_circuit/heckler.rb +8 -6
  67. data/examples/14_rusty_circuit/open_mic.rb +17 -6
  68. data/examples/14_rusty_circuit/scout.rb +17 -10
  69. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +14 -10
  70. data/examples/15_memory_network_and_bus/linux_writer.rb +2 -2
  71. data/examples/15_memory_network_and_bus/os_editor.rb +3 -1
  72. data/examples/15_memory_network_and_bus/os_writer.rb +4 -1
  73. data/examples/16_writers_room/writer.rb +22 -22
  74. data/examples/16_writers_room/writers_room.rb +2 -0
  75. data/examples/17_skills.rb +14 -13
  76. data/examples/18_rails/README.md +20 -1
  77. data/examples/18_rails/app/controllers/chat_controller.rb +5 -1
  78. data/examples/18_rails/app/jobs/robot_run_job.rb +11 -5
  79. data/examples/18_rails/app/robots/chat_robot.rb +11 -0
  80. data/examples/18_rails/config/initializers/robot_lab.rb +8 -0
  81. data/examples/19_token_tracking.rb +25 -9
  82. data/examples/20_circuit_breaker.rb +10 -7
  83. data/examples/21_learning_loop.rb +42 -16
  84. data/examples/22_context_compression.rb +23 -23
  85. data/examples/23_convergence.rb +24 -17
  86. data/examples/24_structured_delegation.rb +13 -8
  87. data/examples/25_history_search.rb +12 -8
  88. data/examples/27_incident_response/incident_response.rb +31 -13
  89. data/examples/28_mcp_discovery.rb +17 -13
  90. data/examples/29_ractor_tools.rb +4 -2
  91. data/examples/30_ractor_network.rb +22 -17
  92. data/examples/31_launch_assessment.rb +20 -9
  93. data/examples/32_newsletter_reader.rb +7 -2
  94. data/examples/33_stock_predictor.rb +34 -13
  95. data/examples/34_agentskills.rb +7 -3
  96. data/examples/35_hooks.rb +18 -8
  97. data/examples/README.md +199 -45
  98. data/examples/common.rb +79 -11
  99. data/examples/xyzzy.rb +8 -1
  100. data/lib/robot_lab/config.rb +10 -5
  101. data/lib/robot_lab/names.rb +402 -0
  102. data/lib/robot_lab/robot/agent_skill_matching.rb +1 -3
  103. data/lib/robot_lab/robot/bus_messaging.rb +16 -8
  104. data/lib/robot_lab/robot/template_rendering.rb +16 -3
  105. data/lib/robot_lab/robot.rb +23 -2
  106. data/lib/robot_lab/version.rb +1 -1
  107. data/lib/robot_lab.rb +21 -15
  108. data/mkdocs.yml +6 -1
  109. metadata +7 -2
@@ -83,14 +83,33 @@ Retrieve one or more values, optionally waiting until they exist.
83
83
 
84
84
  **Parameters:**
85
85
 
86
- | Name | Type | Description |
87
- |------|------|-------------|
88
- | `keys` | `Symbol`, `String` | One or more keys to retrieve |
89
- | `wait` | `Boolean`, `Numeric` | `false`: immediate, `true`: block, `Numeric`: timeout |
90
-
91
- **Returns:** Single value for one key, `Hash` for multiple keys.
92
-
93
- **Raises:** `AwaitTimeout` if timeout expires.
86
+ | Name | Type | Default | Description |
87
+ |------|------|---------|-------------|
88
+ | `*keys` | `Symbol`, `String` | — | One or more keys to retrieve; flattened and symbolized |
89
+ | `wait` | `Boolean`, `Numeric` | `false` | `false`: return immediately (nil if missing). `true`: block indefinitely. `Numeric`: block up to that many seconds |
90
+
91
+ **Returns:** the single value for one key, a `Hash` keyed by symbol for multiple keys.
92
+
93
+ **Raises:** `RobotLab::AwaitTimeout` `"Timeout waiting for :<key> after <N> seconds"`.
94
+
95
+ !!! warning "Blocking semantics"
96
+ - On expiry `get` **raises `RobotLab::AwaitTimeout`**; it does not return nil.
97
+ Rescue it if a missing value is acceptable.
98
+ - With multiple keys the timeout is applied **per missing key**, not to the
99
+ call as a whole. `get(:a, :b, :c, wait: 60)` can block for up to 180
100
+ seconds if all three are missing.
101
+ - `wait: true` blocks with no deadline and can hang forever. Prefer a numeric
102
+ timeout in production.
103
+ - Waiting is implemented with a pipe (`Waiter`, using `IO#wait_readable`), so
104
+ a blocked reader does not spin.
105
+
106
+ ```ruby
107
+ value = begin
108
+ memory.get(:sentiment, wait: 30)
109
+ rescue RobotLab::AwaitTimeout
110
+ nil
111
+ end
112
+ ```
94
113
 
95
114
  ### key?
96
115
 
@@ -114,7 +133,16 @@ Remove a key. Cannot delete reserved keys.
114
133
  memory.keys # => Array<Symbol>
115
134
  ```
116
135
 
117
- Get all non-reserved keys.
136
+ All keys **excluding** the reserved ones.
137
+
138
+ ### all_keys
139
+
140
+ ```ruby
141
+ memory.all_keys # => Array<Symbol>
142
+ ```
143
+
144
+ All keys **including** the reserved ones (`:data`, `:results`, `:messages`,
145
+ `:session_id`, `:cache`).
118
146
 
119
147
  ### clear
120
148
 
@@ -130,7 +158,14 @@ Clear all non-reserved keys.
130
158
  memory.reset
131
159
  ```
132
160
 
133
- Reset memory to initial state (clears everything including reserved keys, preserves cache).
161
+ Reset memory to its initial state: clears the backend, restores `:data` to `{}`,
162
+ `:results` and `:messages` to `[]`, `:session_id` to `nil`, and re-installs the
163
+ existing cache object. The `StateProxy` returned by `data` is discarded and
164
+ rebuilt on next access.
165
+
166
+ This resets the **key-value store only**. It has nothing to do with a robot's
167
+ chat history — use `robot.clear_messages(keep_system: true)` for that. The two
168
+ are independent.
134
169
 
135
170
  ### subscribe
136
171
 
@@ -141,20 +176,41 @@ end
141
176
  ```
142
177
 
143
178
  Subscribe to changes on one or more keys. Callback receives a `MemoryChange` object.
179
+ Returns a subscription ID for [`unsubscribe`](#unsubscribe).
180
+
181
+ **Raises:** `ArgumentError` if no block is given.
182
+
183
+ Only [`set`](#set) notifies subscribers. Writing a reserved key
184
+ (`memory[:data] = ...`, `session_id=`, `append_result`) bypasses notification
185
+ entirely, as does mutating `memory.data`.
144
186
 
145
- **MemoryChange attributes:**
187
+ Callbacks are dispatched through `Async { }`. **Outside a running reactor the
188
+ callback runs synchronously on the writer's thread**, so a slow subscriber blocks
189
+ the `set` that triggered it.
190
+
191
+ **`RobotLab::MemoryChange` attributes:**
146
192
 
147
193
  | Attribute | Type | Description |
148
194
  |-----------|------|-------------|
149
195
  | `key` | `Symbol` | The changed key |
150
196
  | `value` | `Object` | New value |
151
- | `previous` | `Object` | Previous value |
152
- | `writer` | `String, nil` | Name of robot that wrote |
197
+ | `previous` | `Object, nil` | Previous value |
198
+ | `writer` | `String, nil` | `memory.current_writer` at the time of the write — set to the robot's name for the duration of each `run` |
153
199
  | `network_name` | `String, nil` | Network name |
154
200
  | `timestamp` | `Time` | When the change occurred |
155
- | `created?` | `Boolean` | Previous was nil |
156
- | `updated?` | `Boolean` | Previous was not nil |
157
- | `deleted?` | `Boolean` | New value is nil |
201
+ | `correlation_id` | `String, nil` | Optional tracing ID |
202
+
203
+ **Predicates** note that each tests *both* sides, so all three are false when
204
+ `previous` and `value` are both nil, and all three are false for a nil→nil write:
205
+
206
+ | Predicate | Exact definition |
207
+ |-----------|------------------|
208
+ | `created?` | `previous.nil? && !value.nil?` |
209
+ | `updated?` | `!previous.nil? && !value.nil?` |
210
+ | `deleted?` | `value.nil? && !previous.nil?` |
211
+
212
+ Also available: `to_h` (`.compact`ed, `timestamp` rendered as ISO-8601),
213
+ `to_json`, and `MemoryChange.from_hash`.
158
214
 
159
215
  ### subscribe_pattern
160
216
 
@@ -164,7 +220,8 @@ sub_id = memory.subscribe_pattern("analysis:*") do |change|
164
220
  end
165
221
  ```
166
222
 
167
- Subscribe to keys matching a glob pattern (`*` and `?` supported).
223
+ Subscribe to keys matching a glob pattern (`*` and `?` supported). Returns a
224
+ subscription ID. **Raises:** `ArgumentError` if no block is given.
168
225
 
169
226
  ### unsubscribe
170
227
 
@@ -172,15 +229,131 @@ Subscribe to keys matching a glob pattern (`*` and `?` supported).
172
229
  memory.unsubscribe(sub_id) # => Boolean
173
230
  ```
174
231
 
175
- Remove a subscription by its ID.
232
+ Remove a subscription by its ID (works for both `subscribe` and
233
+ `subscribe_pattern`). Returns `true` when something was removed.
234
+
235
+ ### unsubscribe_keys
236
+
237
+ ```ruby
238
+ memory.unsubscribe_keys(:status, :progress) # => self
239
+ ```
240
+
241
+ Drop **all** key subscriptions for the named keys at once, without needing their
242
+ IDs. Pattern subscriptions are unaffected.
243
+
244
+ ### subscribed?
245
+
246
+ ```ruby
247
+ memory.subscribed?(:status) # => Boolean
248
+ ```
249
+
250
+ Whether any subscriber — key-based or pattern-based — would be notified for `key`.
176
251
 
177
252
  ### merge!
178
253
 
179
254
  ```ruby
180
- memory.merge!(key1: "value1", key2: "value2")
255
+ memory.merge!(key1: "value1", key2: "value2") # => self
256
+ ```
257
+
258
+ Merge multiple key-value pairs into memory. Each pair is assigned through `[]=`,
259
+ so non-reserved keys go through the reactive `set` path and do notify subscribers.
260
+
261
+ ## Results and History
262
+
263
+ ### append_result
264
+
265
+ ```ruby
266
+ memory.append_result(robot_result) # => self
267
+ ```
268
+
269
+ Push a `RobotResult` onto the accumulated `:results` array. Bypasses subscriber
270
+ notification.
271
+
272
+ ### set_results
273
+
274
+ ```ruby
275
+ memory.set_results(array_of_results) # => self
276
+ ```
277
+
278
+ Replace the whole `:results` array (used when loading from persistence).
279
+
280
+ ### results_from
281
+
282
+ ```ruby
283
+ memory.results_from(5) # => Array<RobotResult>
181
284
  ```
182
285
 
183
- Merge multiple key-value pairs into memory.
286
+ Results from the given index onward — for incremental saves. Returns `[]` when
287
+ the index is past the end.
288
+
289
+ ### format_history
290
+
291
+ ```ruby
292
+ memory.format_history(formatter: nil) # => Array<Message>
293
+ ```
294
+
295
+ `messages` followed by every result flat-mapped through `formatter`. Pass a
296
+ `Proc` for `formatter:` to control how a `RobotResult` becomes messages; the
297
+ default formatter is used when omitted.
298
+
299
+ ## Backend
300
+
301
+ ### redis?
302
+
303
+ ```ruby
304
+ memory.redis? # => Boolean
305
+ ```
306
+
307
+ Whether this memory is backed by Redis rather than the in-process Hash. The
308
+ `backend: :auto` default tries Redis and falls back to a Hash; `backend: :hash`
309
+ forces the Hash.
310
+
311
+ ### network_name
312
+
313
+ ```ruby
314
+ memory.network_name # => String, nil
315
+ ```
316
+
317
+ The network this memory belongs to, set once at construction and read-only
318
+ thereafter. `Network` creates its shared memory as `Memory.new(network_name: name)`;
319
+ a standalone robot's inherent memory has `nil`. It is copied onto every
320
+ `MemoryChange` so a subscriber can tell which network a write came from, and it is
321
+ preserved by `clone`/`dup` — but **not** by `from_hash`.
322
+
323
+ ### current_writer / current_writer=
324
+
325
+ ```ruby
326
+ memory.current_writer # => String, nil
327
+ memory.current_writer = "bot"
328
+ ```
329
+
330
+ The name attributed to writes, surfaced as `MemoryChange#writer`. `Robot#run`
331
+ sets this to the robot's name for the duration of the run and restores the
332
+ previous value in an `ensure` block, so nested and concurrent runs attribute
333
+ correctly.
334
+
335
+ ## Document Store
336
+
337
+ These four methods require the **`robot_lab-document_store`** extension gem. Without
338
+ it every one of them raises
339
+ `RobotLab::DependencyError: document storage requires the robot_lab-document_store gem.`
340
+
341
+ | Method | Returns | Description |
342
+ |--------|---------|-------------|
343
+ | `store_document(key, text)` | `self` | Embed `text` and store it under `key` |
344
+ | `search_documents(query, limit: 5)` | `Array<Hash>` | Hits sorted by score descending; each hash has `:key`, `:text`, `:score` |
345
+ | `document_keys` | `Array<Symbol>` | Keys of all stored documents |
346
+ | `delete_document(key)` | `self` | Remove a document |
347
+
348
+ ```ruby
349
+ memory.store_document(:readme, File.read("README.md"))
350
+ memory.search_documents("how to configure redis", limit: 3).each do |hit|
351
+ puts "#{hit[:key]} (#{hit[:score].round(3)})"
352
+ end
353
+ ```
354
+
355
+ Documents live in a separate store, not in the key-value backend — they do not
356
+ appear in `keys`, `all_keys`, or `to_h`.
184
357
 
185
358
  ## Reserved Key Accessors
186
359
 
@@ -193,7 +366,9 @@ memory.data.user_id # Method access
193
366
  memory.data[:status] = "active"
194
367
  ```
195
368
 
196
- Runtime data accessed through `StateProxy` for method-style access.
369
+ Runtime data accessed through a [`StateProxy`](state.md) for method-style access.
370
+ Writes made through the proxy are **not** reactive — they bypass `set`, so they
371
+ wake no blocking readers and notify no subscribers.
197
372
 
198
373
  ### results
199
374
 
@@ -221,10 +396,22 @@ memory.session_id = "abc" # Set session identifier
221
396
  ### cache
222
397
 
223
398
  ```ruby
224
- memory.cache # => RubyLLM::SemanticCache
399
+ memory.cache # => RubyLLM::SemanticCache (the module itself), or nil
225
400
  ```
226
401
 
227
- Semantic cache module (read-only after initialization).
402
+ Read-only after initialization — assigning it raises
403
+ `ArgumentError: Cannot reassign cache - it is initialized automatically`.
404
+
405
+ !!! warning "`cache` is `nil` when caching is disabled"
406
+ The value stored is the `RubyLLM::SemanticCache` **module**, not an instance.
407
+ When constructed with `enable_cache: false` it is `nil`, so
408
+ `memory.cache.fetch(...)` raises `NoMethodError`. Guard on
409
+ `memory.cache` before use, or leave `enable_cache` at its `true` default.
410
+
411
+ ```ruby
412
+ RobotLab::Memory.new.cache # => RubyLLM::SemanticCache
413
+ RobotLab::Memory.new(enable_cache: false).cache # => nil
414
+ ```
228
415
 
229
416
  ## Serialization
230
417
 
@@ -235,27 +422,50 @@ memory.to_h
235
422
  # => { data: {...}, results: [...], messages: [...], session_id: "...", custom: {...} }
236
423
  ```
237
424
 
425
+ Keys: `data` (from the `StateProxy`), `results` (each via `RobotResult#export`),
426
+ `messages` (each via `to_h`), `session_id`, and `custom` (every non-reserved key).
427
+ `cache` is never serialized.
428
+
429
+ !!! warning "`to_h` is `.compact`ed"
430
+ Nil values are dropped, so `session_id` disappears entirely when unset:
431
+
432
+ ```ruby
433
+ RobotLab::Memory.new.to_h
434
+ # => { data: {}, results: [], messages: [], custom: {} } -- no :session_id
435
+ ```
436
+
437
+ Consumers must tolerate the missing key. `custom` is always present (it is
438
+ `{}` rather than nil when there are no custom keys).
439
+
238
440
  ### to_json
239
441
 
240
442
  ```ruby
241
443
  memory.to_json # => String
242
444
  ```
243
445
 
446
+ Serializes `to_h`.
447
+
244
448
  ### from_hash
245
449
 
246
450
  ```ruby
247
- memory = Memory.from_hash(hash)
451
+ memory = RobotLab::Memory.from_hash(hash)
248
452
  ```
249
453
 
250
- Reconstruct memory from a hash.
454
+ Reconstructs `data`, `results`, `messages`, and `session_id`, then re-applies
455
+ every entry from `custom`. A fresh cache is created; subscriptions, backend
456
+ choice, and `network_name` are **not** restored.
251
457
 
252
- ### clone
458
+ ### clone / dup
253
459
 
254
460
  ```ruby
255
461
  new_memory = memory.clone
462
+ new_memory = memory.dup # alias for clone
256
463
  ```
257
464
 
258
- Deep copy with fresh subscriptions (cache and network_name preserved).
465
+ Deep copy of `data` plus copies of `results`, `messages`, `session_id`, and all
466
+ non-reserved keys. The `enable_cache` setting and `network_name` are preserved.
467
+ Subscriptions are **not** copied — the clone starts with none. Copying custom
468
+ keys uses the internal non-reactive setter, so no notifications fire.
259
469
 
260
470
  ## Examples
261
471
 
@@ -297,11 +507,18 @@ network.memory.set(:sentiment, { score: 0.8, confidence: 0.95 })
297
507
 
298
508
  # In robot B (reader, may run concurrently)
299
509
  result = network.memory.get(:sentiment, wait: true) # Block indefinitely
300
- result = network.memory.get(:sentiment, wait: 30) # Block up to 30s
510
+ result = network.memory.get(:sentiment, wait: 30) # Block up to 30s, then raise
301
511
 
302
- # Multiple keys with timeout
512
+ # Multiple keys the timeout applies PER MISSING KEY, so this can block 180s
303
513
  results = network.memory.get(:sentiment, :entities, :keywords, wait: 60)
304
514
  # => { sentiment: {...}, entities: [...], keywords: [...] }
515
+
516
+ # Treat a timeout as "not available"
517
+ sentiment = begin
518
+ network.memory.get(:sentiment, wait: 30)
519
+ rescue RobotLab::AwaitTimeout
520
+ nil
521
+ end
305
522
  ```
306
523
 
307
524
  ### Reactive Subscriptions
@@ -357,4 +574,5 @@ memory = Memory.from_hash(data)
357
574
  ## See Also
358
575
 
359
576
  - [Memory Guide](../../guides/memory.md)
577
+ - [StateProxy](state.md) — the wrapper returned by `memory.data`
360
578
  - [State Management Architecture](../../architecture/state-management.md)