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
@@ -2,114 +2,93 @@
2
2
 
3
3
  Stream LLM responses in real-time for better user experience.
4
4
 
5
- ## Streaming via Callbacks
5
+ RobotLab has exactly two ways to receive streaming content, and both hand you a
6
+ `RubyLLM::Chunk`:
6
7
 
7
- RobotLab robots support streaming through callback methods inherited from RubyLLM::Agent. Register callbacks before calling `run`:
8
+ 1. **`on_content:`** a callback wired at build time (or through `RunConfig`) that fires on *every* `run`
9
+ 2. **A block passed to `run`** — `robot.run(msg) { |chunk| ... }`, for one-off streaming
10
+
11
+ > [!IMPORTANT]
12
+ > Read the content off the chunk with **`chunk.content`**. `RubyLLM::Chunk`
13
+ > has no `#text` method — `chunk.text` raises `NoMethodError`.
14
+
15
+ ## Streaming via `on_content:`
16
+
17
+ Wire the callback once at build time; it fires on every subsequent `run`:
8
18
 
9
19
  ```ruby
10
20
  robot = RobotLab.build(
11
21
  name: "storyteller",
12
- system_prompt: "You are a creative storyteller."
22
+ system_prompt: "You are a creative storyteller.",
23
+ on_content: ->(chunk) { print chunk.content }
13
24
  )
14
25
 
15
- # Register streaming callback
16
- robot.on_new_message do |message|
17
- print message.content if message.content
18
- end
19
-
20
26
  result = robot.run("Tell me a story about a brave robot")
27
+ puts
28
+ puts result.last_text_content
21
29
  ```
22
30
 
23
- ## Available Callbacks
31
+ ## Streaming via a Block on `run`
24
32
 
25
- ### on_new_message
26
-
27
- Called when the assistant starts generating a new message, with streaming chunks:
33
+ For one-off streaming, pass a block straight to `run`:
28
34
 
29
35
  ```ruby
30
- robot.on_new_message do |message|
31
- print message.content if message.content
32
- end
33
- ```
34
-
35
- ### on_end_message
36
-
37
- Called when the assistant finishes a message:
36
+ robot = RobotLab.build(
37
+ name: "factbot",
38
+ system_prompt: "You are concise. Answer in one sentence."
39
+ )
38
40
 
39
- ```ruby
40
- robot.on_end_message do |message|
41
- puts "\n--- Response complete ---"
42
- puts "Content length: #{message.content&.length}"
43
- end
41
+ robot.run("What year was Ruby created?") { |chunk| print chunk.content }
44
42
  ```
45
43
 
46
- ### on_tool_call
47
-
48
- Called when the LLM invokes a tool:
44
+ The block goes to the same place `on_content:` does — no need to reach for
45
+ `robot.chat.ask` to get streaming, and reaching for it skips memory resolution,
46
+ tool resolution, hooks, and budget accounting.
49
47
 
50
- ```ruby
51
- robot.on_tool_call do |tool_call|
52
- puts "Calling tool: #{tool_call.name}"
53
- end
54
- ```
48
+ ## Using Both Together
55
49
 
56
- ### on_tool_result
57
-
58
- Called when a tool returns its result:
50
+ When a robot has a stored `on_content:` *and* a block is passed to `run`, both
51
+ fire for every chunk. The **stored callback fires first**, then the block:
59
52
 
60
53
  ```ruby
61
- robot.on_tool_result do |tool_call, result|
62
- puts "Tool #{tool_call.name} returned: #{result}"
63
- end
64
- ```
65
-
66
- ## Comprehensive Callback Setup
67
-
68
- Register all callbacks for full visibility:
54
+ stored_log = []
55
+ block_log = []
69
56
 
70
- ```ruby
71
57
  robot = RobotLab.build(
72
- name: "assistant",
73
- system_prompt: "You are helpful.",
74
- local_tools: [WeatherTool]
58
+ name: "combo",
59
+ system_prompt: "You are concise.",
60
+ on_content: ->(chunk) { stored_log << chunk.content }
75
61
  )
76
62
 
77
- robot.on_new_message do |message|
78
- print message.content if message.content
79
- end
80
-
81
- robot.on_end_message do |_message|
82
- puts "\n--- Done ---"
83
- end
84
-
85
- robot.on_tool_call do |tool_call|
86
- puts "\n[Tool] Calling: #{tool_call.name}"
87
- end
88
-
89
- robot.on_tool_result do |tool_call, result|
90
- puts "[Tool] #{tool_call.name} returned: #{result}"
91
- end
63
+ robot.run("What is Matz's full name?") { |chunk| block_log << chunk.content }
92
64
 
93
- result = robot.run("What's the weather in Tokyo?")
65
+ stored_log == block_log # => true — both saw the same chunks
94
66
  ```
95
67
 
96
- ## Streaming via Chat Block
68
+ ## Streaming via RunConfig
97
69
 
98
- For more control, pass a block directly to `chat.ask` (the underlying RubyLLM method):
70
+ `on_content` is a `RunConfig` field, so it participates in the config cascade:
99
71
 
100
72
  ```ruby
73
+ config = RobotLab::RunConfig.new(
74
+ model: "claude-sonnet-4",
75
+ on_content: ->(chunk) { print chunk.content }
76
+ )
77
+
101
78
  robot = RobotLab.build(
102
- name: "chat_bot",
103
- system_prompt: "You are a helpful assistant."
79
+ name: "config_bot",
80
+ system_prompt: "You are concise.",
81
+ config: config
104
82
  )
105
83
 
106
- # Use the underlying chat directly with a streaming block
107
- robot.chat.ask("Tell me a story") do |chunk|
108
- print chunk.content if chunk.content
109
- end
84
+ robot.run("Who designed the Ruby programming language?")
110
85
  ```
111
86
 
112
- Note: Using `chat.ask` directly bypasses Robot's memory resolution and tool hierarchy. Use callbacks with `robot.run` when you need those features.
87
+ > [!WARNING]
88
+ > `on_content` is read from the **robot's own** config when the robot is
89
+ > constructed. A network-level `config:` propagates only `mcp` and `tools` to
90
+ > member robots — it will **not** supply `on_content` to them. Put the callback
91
+ > on each robot (or on the `config:` you pass to that robot).
113
92
 
114
93
  ## Web Integration
115
94
 
@@ -120,18 +99,14 @@ class ChatChannel < ApplicationCable::Channel
120
99
  def receive(data)
121
100
  robot = RobotLab.build(
122
101
  name: "chat_bot",
123
- system_prompt: "You are a helpful chat assistant."
102
+ system_prompt: "You are a helpful chat assistant.",
103
+ on_content: ->(chunk) {
104
+ transmit({ event: "text.delta", content: chunk.content })
105
+ }
124
106
  )
125
107
 
126
- robot.on_new_message do |message|
127
- transmit({ event: "text.delta", content: message.content }) if message.content
128
- end
129
-
130
- robot.on_end_message do |_message|
131
- transmit({ event: "run.completed" })
132
- end
133
-
134
108
  robot.run(data["message"])
109
+ transmit({ event: "run.completed" })
135
110
  end
136
111
  end
137
112
  ```
@@ -150,15 +125,11 @@ class StreamController < ApplicationController
150
125
  system_prompt: "You are helpful."
151
126
  )
152
127
 
153
- robot.on_new_message do |message|
154
- response.stream.write("data: #{message.content}\n\n") if message.content
128
+ robot.run(params[:message]) do |chunk|
129
+ response.stream.write("data: #{chunk.content}\n\n")
155
130
  end
156
131
 
157
- robot.on_end_message do |_message|
158
- response.stream.write("data: [DONE]\n\n")
159
- end
160
-
161
- robot.run(params[:message])
132
+ response.stream.write("data: [DONE]\n\n")
162
133
  ensure
163
134
  response.stream.close
164
135
  end
@@ -170,95 +141,183 @@ end
170
141
  ```ruby
171
142
  # Using Faye WebSocket
172
143
  ws.on :message do |msg|
173
- robot.on_new_message do |message|
174
- ws.send(message.content) if message.content
175
- end
176
-
177
- robot.run(msg.data)
144
+ robot.run(msg.data) { |chunk| ws.send(chunk.content) }
178
145
  end
179
146
  ```
180
147
 
181
148
  ## Progress Tracking
182
149
 
183
- Track streaming progress with callbacks:
150
+ Chunk callbacks are the right place to count characters. Tool activity is
151
+ tracked separately, through the robot's `on_tool_call:` / `on_tool_result:`
152
+ callbacks:
184
153
 
185
154
  ```ruby
186
155
  class StreamProgress
156
+ attr_reader :chars, :tools
157
+
187
158
  def initialize
188
159
  @chars = 0
189
160
  @tools = 0
190
161
  end
191
162
 
192
- attr_reader :chars, :tools
193
-
194
- def attach(robot)
195
- robot.on_new_message do |message|
196
- @chars += message.content.length if message.content
163
+ # Returns the streaming block to hand to run().
164
+ def content_callback
165
+ ->(chunk) {
166
+ @chars += chunk.content.to_s.length
197
167
  print "\rReceived #{@chars} characters..."
198
- end
168
+ }
169
+ end
199
170
 
200
- robot.on_tool_call do |tool_call|
171
+ def tool_callback
172
+ ->(tool_call) {
201
173
  @tools += 1
202
174
  puts "\nTool call ##{@tools}: #{tool_call.name}"
203
- end
175
+ }
204
176
  end
205
177
  end
206
178
 
207
179
  progress = StreamProgress.new
208
- progress.attach(robot)
209
180
 
210
- result = robot.run("Process this complex request")
181
+ robot = RobotLab.build(
182
+ name: "assistant",
183
+ system_prompt: "You are helpful.",
184
+ local_tools: [WeatherTool],
185
+ on_content: progress.content_callback,
186
+ on_tool_call: progress.tool_callback
187
+ )
188
+
189
+ robot.run("Process this complex request", tools: :inherit)
211
190
  puts "\nTotal: #{progress.chars} chars, #{progress.tools} tool calls"
212
191
  ```
213
192
 
193
+ > [!NOTE]
194
+ > `chunk.content` can be `nil` on chunks that carry only metadata (tool-call
195
+ > deltas, usage). Guard with `to_s` when accumulating, as above.
196
+
197
+ > [!WARNING]
198
+ > `run` defaults to `tools: :none`, so the tool callback above never fires
199
+ > unless you pass `tools: :inherit` (or an explicit allowlist). See
200
+ > [Using Tools](using-tools.md#runtime-tool-filtering).
201
+
214
202
  ## Without Streaming
215
203
 
216
- When streaming is not needed, simply call `run` without registering callbacks:
204
+ When streaming is not needed, call `run` with no `on_content:` and no block:
217
205
 
218
206
  ```ruby
219
- # No streaming - returns RobotResult directly
220
207
  result = robot.run("Hello!")
221
208
  puts result.last_text_content
222
209
  ```
223
210
 
224
- ## Best Practices
211
+ ## Tool Callbacks
212
+
213
+ `on_tool_call:` and `on_tool_result:` are constructor/`RunConfig` callbacks
214
+ alongside `on_content:`. Each receives **one** argument:
225
215
 
226
- ### 1. Register Callbacks Before Run
216
+ ```ruby
217
+ robot = RobotLab.build(
218
+ name: "assistant",
219
+ system_prompt: "You are helpful.",
220
+ local_tools: [WeatherTool],
221
+ on_tool_call: ->(tool_call) { puts "\n[Tool] Calling: #{tool_call.name}" },
222
+ on_tool_result: ->(result) { puts "[Tool] returned: #{result}" }
223
+ )
224
+
225
+ robot.run("What's the weather in Tokyo?", tools: :inherit)
226
+ ```
227
+
228
+ Note that `on_tool_result` does **not** receive the originating tool call —
229
+ only the result. If you need to correlate the two, record the call in
230
+ `on_tool_call` and pair them up yourself.
231
+
232
+ > [!NOTE]
233
+ > RobotLab installs these two callbacks on the underlying chat's
234
+ > `on_tool_call` / `on_tool_result` hooks, which RubyLLM 1.16 has deprecated.
235
+ > Building a robot with either callback therefore prints a RubyLLM deprecation
236
+ > warning. The callbacks themselves work; the warning comes from the layer
237
+ > below.
238
+
239
+ ## Legacy RubyLLM Chat Callbacks
240
+
241
+ `Robot` also exposes RubyLLM's chat-level callbacks
242
+ (`on_new_message`, `on_end_message`, `on_tool_call`, `on_tool_result`).
243
+
244
+ > [!CAUTION]
245
+ > All four are **deprecated in RubyLLM 1.16** and slated for removal in
246
+ > RubyLLM 2.0. Registering one emits a deprecation warning naming its
247
+ > replacement: `before_message`, `after_message`, `before_tool_call`, and
248
+ > `after_tool_result`. Prefer RobotLab's `on_content:` /
249
+ > `on_tool_call:` / `on_tool_result:` constructor callbacks instead.
250
+
251
+ Their arities are not what you might expect:
252
+
253
+ | Callback | Replacement | Arguments received |
254
+ |----------|-------------|--------------------|
255
+ | `on_new_message` | `before_message` | **none** |
256
+ | `on_end_message` | `after_message` | the completed `RubyLLM::Message` |
257
+ | `on_tool_call` | `before_tool_call` | the `ToolCall` |
258
+ | `on_tool_result` | `after_tool_result` | the **result only** |
259
+
260
+ > [!WARNING]
261
+ > `on_new_message` is **not** a streaming hook. It fires once when a message
262
+ > begins and is invoked with **zero arguments** — a block written as
263
+ > `do |message| ... end` receives `nil`, so `message.content` raises
264
+ > `NoMethodError: undefined method 'content' for nil`. Use `on_content:` or a
265
+ > `run` block for content.
227
266
 
228
267
  ```ruby
229
- # Correct: register first, then run
230
- robot.on_new_message { |msg| print msg.content if msg.content }
231
- robot.run("Hello")
268
+ # Correct usage of the legacy hooks (still deprecated):
269
+ robot.chat.on_new_message { puts "--- message starting ---" } # no argument
270
+ robot.chat.on_end_message { |message| puts "Length: #{message.content&.length}" }
271
+ robot.chat.on_tool_result { |result| puts "Tool returned: #{result}" }
232
272
  ```
233
273
 
234
- ### 2. Handle Errors in Callbacks
274
+ ## Best Practices
275
+
276
+ ### 1. Wire streaming before the run
277
+
278
+ `on_content:` belongs in the constructor (or the `config:` you pass it); a
279
+ `run` block belongs on the call itself. There is no "register then run" step.
280
+
281
+ ### 2. Handle errors inside the callback
282
+
283
+ An exception raised in a streaming callback propagates out of `run` and aborts
284
+ the turn. Swallow client-side failures you can survive:
235
285
 
236
286
  ```ruby
237
- robot.on_new_message do |message|
238
- begin
239
- broadcast(message.content) if message.content
287
+ robot = RobotLab.build(
288
+ name: "broadcaster",
289
+ system_prompt: "You are helpful.",
290
+ on_content: ->(chunk) do
291
+ broadcast(chunk.content)
240
292
  rescue BroadcastError => e
241
- # Client disconnected, but continue processing
242
- logger.warn "Broadcast failed: #{e.message}"
293
+ # Client disconnected, but let the run finish.
294
+ RobotLab.config.logger.warn("Broadcast failed: #{e.message}")
243
295
  end
244
- end
296
+ )
245
297
  ```
246
298
 
247
- ### 3. Clean Up Resources
299
+ ### 3. Clean up resources
248
300
 
249
301
  ```ruby
250
302
  begin
251
- robot.on_new_message do |message|
252
- stream_to_client(message.content) if message.content
253
- end
254
- robot.run("Hello")
303
+ robot.run("Hello") { |chunk| stream_to_client(chunk.content) }
255
304
  ensure
256
305
  close_stream_connection
257
306
  end
258
307
  ```
259
308
 
309
+ ## What About `RobotLab::Streaming`?
310
+
311
+ `RobotLab::Streaming::Context`, `Streaming::Events`, and
312
+ `Streaming::SequenceCounter` exist in the codebase and are documented in the
313
+ [API reference](../api/streaming/index.md), but **nothing in `Robot` or
314
+ `Network` constructs or publishes to them**. They are a standalone event-object
315
+ toolkit you would have to drive yourself — not the path that `run` takes. For
316
+ actual token streaming, use `on_content:` or a `run` block as shown above.
317
+
260
318
  ## Next Steps
261
319
 
262
320
  - [Building Robots](building-robots.md) - Robot creation
263
321
  - [Creating Networks](creating-networks.md) - Network patterns
264
- - [API Reference: Streaming](../api/streaming/index.md) - Complete API
322
+ - [examples/05_streaming.rb](https://github.com/MadBomber/robot_lab/blob/main/examples/05_streaming.rb) - Runnable streaming example
323
+ - [API Reference: Streaming](../api/streaming/index.md) - The standalone `Streaming::*` event objects