robot_lab 0.2.1 → 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 (236) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +1 -0
  3. data/.loki +60 -0
  4. data/.quality/reek_baseline.txt +43 -0
  5. data/.rubocop.yml +5 -167
  6. data/CHANGELOG.md +54 -0
  7. data/README.md +185 -64
  8. data/Rakefile +28 -12
  9. data/docs/api/core/index.md +41 -14
  10. data/docs/api/core/memory.md +247 -29
  11. data/docs/api/core/network.md +285 -33
  12. data/docs/api/core/result.md +120 -32
  13. data/docs/api/core/robot.md +636 -60
  14. data/docs/api/core/state.md +87 -197
  15. data/docs/api/core/tool.md +165 -20
  16. data/docs/api/errors.md +152 -14
  17. data/docs/api/hooks.md +469 -0
  18. data/docs/api/index.md +83 -7
  19. data/docs/api/mcp/client.md +129 -35
  20. data/docs/api/mcp/index.md +164 -23
  21. data/docs/api/mcp/server.md +27 -3
  22. data/docs/api/mcp/transports.md +94 -22
  23. data/docs/api/messages/index.md +26 -3
  24. data/docs/api/messages/text-message.md +33 -11
  25. data/docs/api/messages/tool-call-message.md +27 -4
  26. data/docs/api/messages/tool-result-message.md +23 -4
  27. data/docs/api/messages/user-message.md +45 -8
  28. data/docs/api/skills.md +519 -0
  29. data/docs/api/streaming/context.md +28 -5
  30. data/docs/api/streaming/index.md +57 -11
  31. data/docs/api/support.md +846 -0
  32. data/docs/architecture/core-concepts.md +118 -30
  33. data/docs/architecture/index.md +86 -11
  34. data/docs/architecture/message-flow.md +66 -29
  35. data/docs/architecture/network-orchestration.md +153 -38
  36. data/docs/architecture/robot-execution.md +173 -91
  37. data/docs/architecture/state-management.md +31 -12
  38. data/docs/concepts.md +176 -21
  39. data/docs/examples/basic-chat.md +72 -19
  40. data/docs/examples/index.md +117 -31
  41. data/docs/examples/mcp-server.md +154 -45
  42. data/docs/examples/multi-robot-network.md +91 -21
  43. data/docs/examples/tool-usage.md +104 -37
  44. data/docs/getting-started/configuration.md +299 -93
  45. data/docs/getting-started/installation.md +53 -41
  46. data/docs/getting-started/quick-start.md +51 -6
  47. data/docs/guides/building-robots.md +296 -50
  48. data/docs/guides/creating-networks.md +220 -31
  49. data/docs/guides/hooks.md +1084 -0
  50. data/docs/guides/index.md +5 -0
  51. data/docs/guides/knowledge.md +37 -6
  52. data/docs/guides/mcp-integration.md +211 -44
  53. data/docs/guides/memory.md +103 -12
  54. data/docs/guides/observability.md +166 -51
  55. data/docs/guides/streaming.md +184 -125
  56. data/docs/guides/using-tools.md +300 -11
  57. data/docs/index.md +37 -5
  58. data/examples/01_simple_robot.rb +2 -2
  59. data/examples/02_tools.rb +14 -4
  60. data/examples/03_network.rb +23 -9
  61. data/examples/04_mcp.rb +11 -4
  62. data/examples/05_streaming.rb +8 -5
  63. data/examples/06_prompt_templates.rb +13 -9
  64. data/examples/07_network_memory.rb +5 -5
  65. data/examples/08_llm_config.rb +20 -15
  66. data/examples/09_chaining.rb +4 -4
  67. data/examples/11_network_introspection.rb +4 -4
  68. data/examples/12_message_bus.rb +2 -2
  69. data/examples/13_spawn.rb +2 -2
  70. data/examples/14_rusty_circuit/README.md +1 -0
  71. data/examples/14_rusty_circuit/comic.rb +7 -3
  72. data/examples/14_rusty_circuit/display.rb +14 -0
  73. data/examples/14_rusty_circuit/heckler.rb +8 -6
  74. data/examples/14_rusty_circuit/open_mic.rb +17 -6
  75. data/examples/14_rusty_circuit/scout.rb +17 -10
  76. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +14 -10
  77. data/examples/15_memory_network_and_bus/linux_writer.rb +2 -2
  78. data/examples/15_memory_network_and_bus/os_editor.rb +3 -1
  79. data/examples/15_memory_network_and_bus/os_writer.rb +4 -1
  80. data/examples/16_writers_room/writer.rb +22 -22
  81. data/examples/16_writers_room/writers_room.rb +2 -0
  82. data/examples/17_skills.rb +14 -13
  83. data/examples/18_rails/README.md +20 -1
  84. data/examples/18_rails/app/controllers/chat_controller.rb +5 -1
  85. data/examples/18_rails/app/jobs/robot_run_job.rb +11 -5
  86. data/examples/18_rails/app/robots/chat_robot.rb +11 -0
  87. data/examples/18_rails/config/initializers/robot_lab.rb +8 -0
  88. data/examples/19_token_tracking.rb +25 -9
  89. data/examples/20_circuit_breaker.rb +10 -7
  90. data/examples/21_learning_loop.rb +42 -16
  91. data/examples/22_context_compression.rb +23 -23
  92. data/examples/23_convergence.rb +24 -17
  93. data/examples/24_structured_delegation.rb +13 -8
  94. data/examples/25_history_search.rb +12 -8
  95. data/examples/27_incident_response/incident_response.rb +31 -13
  96. data/examples/28_mcp_discovery.rb +17 -13
  97. data/examples/29_ractor_tools.rb +4 -2
  98. data/examples/30_ractor_network.rb +22 -17
  99. data/examples/31_launch_assessment.rb +20 -9
  100. data/examples/32_newsletter_reader.rb +7 -2
  101. data/examples/33_stock_predictor.rb +34 -13
  102. data/examples/34_agentskills.rb +7 -3
  103. data/examples/35_hooks.rb +266 -0
  104. data/examples/README.md +203 -42
  105. data/examples/common.rb +79 -7
  106. data/examples/xyzzy.rb +97 -0
  107. data/lib/robot_lab/agent_skill.rb +5 -4
  108. data/lib/robot_lab/budget/ledger.rb +98 -0
  109. data/lib/robot_lab/capabilities.rb +84 -0
  110. data/lib/robot_lab/config/defaults.yml +10 -0
  111. data/lib/robot_lab/config.rb +14 -9
  112. data/lib/robot_lab/error.rb +34 -4
  113. data/lib/robot_lab/errors.rb +45 -0
  114. data/lib/robot_lab/hook.rb +79 -0
  115. data/lib/robot_lab/hook_context.rb +194 -0
  116. data/lib/robot_lab/hook_registry.rb +55 -0
  117. data/lib/robot_lab/hooks.rb +87 -0
  118. data/lib/robot_lab/mcp/connection_poller.rb +2 -2
  119. data/lib/robot_lab/names.rb +402 -0
  120. data/lib/robot_lab/narrator.rb +87 -0
  121. data/lib/robot_lab/network.rb +77 -20
  122. data/lib/robot_lab/robot/agent_skill_matching.rb +1 -3
  123. data/lib/robot_lab/robot/budget.rb +89 -0
  124. data/lib/robot_lab/robot/bus_messaging.rb +90 -18
  125. data/lib/robot_lab/robot/hooking.rb +56 -0
  126. data/lib/robot_lab/robot/mcp_management.rb +8 -2
  127. data/lib/robot_lab/robot/template_rendering.rb +18 -5
  128. data/lib/robot_lab/robot.rb +233 -89
  129. data/lib/robot_lab/run_config.rb +24 -5
  130. data/lib/robot_lab/runnable.rb +51 -0
  131. data/lib/robot_lab/sandbox/null.rb +13 -0
  132. data/lib/robot_lab/sandbox/seatbelt.rb +104 -0
  133. data/lib/robot_lab/sandbox.rb +52 -0
  134. data/lib/robot_lab/script_tool.rb +60 -2
  135. data/lib/robot_lab/task.rb +26 -20
  136. data/lib/robot_lab/tool.rb +52 -11
  137. data/lib/robot_lab/version.rb +1 -1
  138. data/lib/robot_lab.rb +70 -15
  139. data/mkdocs.yml +7 -1
  140. metadata +41 -99
  141. data/examples/temp.md +0 -51
  142. data/site/404.html +0 -2300
  143. data/site/api/core/index.html +0 -2706
  144. data/site/api/core/memory/index.html +0 -3793
  145. data/site/api/core/network/index.html +0 -3500
  146. data/site/api/core/robot/index.html +0 -4566
  147. data/site/api/core/state/index.html +0 -3390
  148. data/site/api/core/tool/index.html +0 -3843
  149. data/site/api/index.html +0 -2635
  150. data/site/api/mcp/client/index.html +0 -3435
  151. data/site/api/mcp/index.html +0 -2783
  152. data/site/api/mcp/server/index.html +0 -3252
  153. data/site/api/mcp/transports/index.html +0 -3352
  154. data/site/api/messages/index.html +0 -2641
  155. data/site/api/messages/text-message/index.html +0 -3087
  156. data/site/api/messages/tool-call-message/index.html +0 -3159
  157. data/site/api/messages/tool-result-message/index.html +0 -3252
  158. data/site/api/messages/user-message/index.html +0 -3212
  159. data/site/api/streaming/context/index.html +0 -3282
  160. data/site/api/streaming/events/index.html +0 -3347
  161. data/site/api/streaming/index.html +0 -2738
  162. data/site/architecture/core-concepts/index.html +0 -3757
  163. data/site/architecture/index.html +0 -2797
  164. data/site/architecture/message-flow/index.html +0 -3238
  165. data/site/architecture/network-orchestration/index.html +0 -3433
  166. data/site/architecture/robot-execution/index.html +0 -3140
  167. data/site/architecture/state-management/index.html +0 -3498
  168. data/site/assets/css/custom.css +0 -56
  169. data/site/assets/images/favicon.png +0 -0
  170. data/site/assets/images/robot_lab.jpg +0 -0
  171. data/site/assets/javascripts/bundle.79ae519e.min.js +0 -16
  172. data/site/assets/javascripts/bundle.79ae519e.min.js.map +0 -7
  173. data/site/assets/javascripts/lunr/min/lunr.ar.min.js +0 -1
  174. data/site/assets/javascripts/lunr/min/lunr.da.min.js +0 -18
  175. data/site/assets/javascripts/lunr/min/lunr.de.min.js +0 -18
  176. data/site/assets/javascripts/lunr/min/lunr.du.min.js +0 -18
  177. data/site/assets/javascripts/lunr/min/lunr.el.min.js +0 -1
  178. data/site/assets/javascripts/lunr/min/lunr.es.min.js +0 -18
  179. data/site/assets/javascripts/lunr/min/lunr.fi.min.js +0 -18
  180. data/site/assets/javascripts/lunr/min/lunr.fr.min.js +0 -18
  181. data/site/assets/javascripts/lunr/min/lunr.he.min.js +0 -1
  182. data/site/assets/javascripts/lunr/min/lunr.hi.min.js +0 -1
  183. data/site/assets/javascripts/lunr/min/lunr.hu.min.js +0 -18
  184. data/site/assets/javascripts/lunr/min/lunr.hy.min.js +0 -1
  185. data/site/assets/javascripts/lunr/min/lunr.it.min.js +0 -18
  186. data/site/assets/javascripts/lunr/min/lunr.ja.min.js +0 -1
  187. data/site/assets/javascripts/lunr/min/lunr.jp.min.js +0 -1
  188. data/site/assets/javascripts/lunr/min/lunr.kn.min.js +0 -1
  189. data/site/assets/javascripts/lunr/min/lunr.ko.min.js +0 -1
  190. data/site/assets/javascripts/lunr/min/lunr.multi.min.js +0 -1
  191. data/site/assets/javascripts/lunr/min/lunr.nl.min.js +0 -18
  192. data/site/assets/javascripts/lunr/min/lunr.no.min.js +0 -18
  193. data/site/assets/javascripts/lunr/min/lunr.pt.min.js +0 -18
  194. data/site/assets/javascripts/lunr/min/lunr.ro.min.js +0 -18
  195. data/site/assets/javascripts/lunr/min/lunr.ru.min.js +0 -18
  196. data/site/assets/javascripts/lunr/min/lunr.sa.min.js +0 -1
  197. data/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +0 -1
  198. data/site/assets/javascripts/lunr/min/lunr.sv.min.js +0 -18
  199. data/site/assets/javascripts/lunr/min/lunr.ta.min.js +0 -1
  200. data/site/assets/javascripts/lunr/min/lunr.te.min.js +0 -1
  201. data/site/assets/javascripts/lunr/min/lunr.th.min.js +0 -1
  202. data/site/assets/javascripts/lunr/min/lunr.tr.min.js +0 -18
  203. data/site/assets/javascripts/lunr/min/lunr.vi.min.js +0 -1
  204. data/site/assets/javascripts/lunr/min/lunr.zh.min.js +0 -1
  205. data/site/assets/javascripts/lunr/tinyseg.js +0 -206
  206. data/site/assets/javascripts/lunr/wordcut.js +0 -6708
  207. data/site/assets/javascripts/workers/search.2c215733.min.js +0 -42
  208. data/site/assets/javascripts/workers/search.2c215733.min.js.map +0 -7
  209. data/site/assets/stylesheets/main.484c7ddc.min.css +0 -1
  210. data/site/assets/stylesheets/main.484c7ddc.min.css.map +0 -1
  211. data/site/assets/stylesheets/palette.ab4e12ef.min.css +0 -1
  212. data/site/assets/stylesheets/palette.ab4e12ef.min.css.map +0 -1
  213. data/site/concepts/index.html +0 -3455
  214. data/site/examples/basic-chat/index.html +0 -2880
  215. data/site/examples/index.html +0 -2907
  216. data/site/examples/mcp-server/index.html +0 -3018
  217. data/site/examples/multi-robot-network/index.html +0 -3131
  218. data/site/examples/rails-application/index.html +0 -3329
  219. data/site/examples/tool-usage/index.html +0 -3085
  220. data/site/getting-started/configuration/index.html +0 -3745
  221. data/site/getting-started/index.html +0 -2572
  222. data/site/getting-started/installation/index.html +0 -2981
  223. data/site/getting-started/quick-start/index.html +0 -2942
  224. data/site/guides/building-robots/index.html +0 -4290
  225. data/site/guides/creating-networks/index.html +0 -3858
  226. data/site/guides/index.html +0 -2586
  227. data/site/guides/mcp-integration/index.html +0 -3581
  228. data/site/guides/memory/index.html +0 -3586
  229. data/site/guides/rails-integration/index.html +0 -4019
  230. data/site/guides/streaming/index.html +0 -3157
  231. data/site/guides/using-tools/index.html +0 -3802
  232. data/site/index.html +0 -2671
  233. data/site/search/search_index.json +0 -1
  234. data/site/sitemap.xml +0 -183
  235. data/site/sitemap.xml.gz +0 -0
  236. data/site/tags.json +0 -1
@@ -6,11 +6,22 @@ Customer service system with intelligent routing using SimpleFlow pipelines.
6
6
 
7
7
  This example demonstrates a multi-robot network where a classifier routes customer inquiries to specialized support robots using SimpleFlow's optional task activation.
8
8
 
9
+ The runnable version is
10
+ [`examples/03_network.rb`](https://github.com/MadBomber/robot_lab/blob/main/examples/03_network.rb);
11
+ the snippets here expand it with a fourth specialist and per-task configuration.
12
+
13
+ > [!IMPORTANT]
14
+ > Two names are in play and they are easy to confuse.
15
+ > `result.activate(:x)` takes a **task** name (the first argument to `task`), while
16
+ > `result.with_context(@name.to_sym, ...)` keys the context by the **robot's** name.
17
+ > Activating a task name that was never declared **raises `ArgumentError` and aborts
18
+ > the run** — it is not a silent no-op. Give each robot the same name as its task and
19
+ > both problems disappear.
20
+
9
21
  ## Complete Example
10
22
 
11
23
  ```ruby
12
24
  #!/usr/bin/env ruby
13
- # examples/customer_service.rb
14
25
 
15
26
  require "bundler/setup"
16
27
  require "robot_lab"
@@ -185,17 +196,26 @@ class ClassifierRobot < RobotLab::Robot
185
196
  .with_context(@name.to_sym, robot_result)
186
197
  .continue(robot_result)
187
198
 
188
- # 5. Activate the appropriate optional task based on output
199
+ # 5. Activate the appropriate optional task based on output.
200
+ # These symbols MUST match the task names declared in create_network.
189
201
  category = robot_result.last_text_content.to_s.strip.downcase
190
202
  case category
191
- when /billing/ then new_result.activate(:billing)
192
- when /technical/ then new_result.activate(:technical)
193
- else new_result.activate(:general)
203
+ when /billing/ then new_result.activate(:billing_agent)
204
+ when /technical/ then new_result.activate(:tech_agent)
205
+ else new_result.activate(:general_agent)
194
206
  end
195
207
  end
196
208
  end
197
209
  ```
198
210
 
211
+ > [!WARNING]
212
+ > `activate` validates its argument and **raises**. Activating `:billing` when the
213
+ > network declared `task :billing_agent, ...` aborts the run with
214
+ > `ArgumentError: Step :classifier attempted to activate unknown step :billing`.
215
+ > Activating a task that was *not* declared `depends_on: :optional` raises too:
216
+ > `ArgumentError: Step :classifier attempted to activate non-optional step :billing_agent.`
217
+ > Use `result.activated_steps` to inspect what routing actually did.
218
+
199
219
  !!! note "extract_run_context"
200
220
  The `extract_run_context(result)` method is a protected helper on `Robot`. It extracts `run_params` from the SimpleFlow result context, handles value propagation from previous steps, and separates robot-specific params (`mcp:`, `tools:`, `memory:`, `network_memory:`) from the message and other context.
201
221
 
@@ -245,22 +265,43 @@ end
245
265
 
246
266
  ## Per-Task Configuration
247
267
 
248
- Tasks can have individual context, tools, and MCP servers:
268
+ Tasks can carry their own context, a tool allowlist, and MCP servers:
249
269
 
250
270
  ```ruby
271
+ # Tools must already be ATTACHED to the robot...
272
+ billing_agent = RobotLab.build(
273
+ name: "billing_agent",
274
+ system_prompt: "You handle billing.",
275
+ local_tools: [RefundTool, InvoiceTool, AuditTool]
276
+ )
277
+
251
278
  network = RobotLab.create_network(name: "support") do
252
279
  task :classifier, classifier, depends_on: :none
253
280
  task :billing_agent, billing_agent,
254
281
  context: { department: "billing", escalation_level: 2 },
255
- tools: [RefundTool, InvoiceTool],
282
+ tools: [RefundTool, InvoiceTool], # ...and this SELECTS from them
256
283
  depends_on: :optional
257
284
  task :tech_agent, tech_agent,
258
285
  context: { department: "technical" },
259
286
  mcp: [filesystem_server],
287
+ tools: :inherit, # send every discovered MCP tool
260
288
  depends_on: :optional
261
289
  end
262
290
  ```
263
291
 
292
+ > [!WARNING]
293
+ > Per-task `tools:` is an **allowlist over tools the robot already has** — it never
294
+ > attaches anything. Give a task `tools: [RefundTool]` for a robot whose
295
+ > `local_tools` are empty and it sends zero tools.
296
+ >
297
+ > Matching is by string comparison against each attached tool's `name`, so the two
298
+ > sides must agree. `local_tools: [RefundTool]` (the class) matches
299
+ > `tools: [RefundTool]`, but `local_tools: [RefundTool.new]` (an instance, whose
300
+ > name is `"refund"`) does not — use `tools: %w[refund]` there.
301
+ >
302
+ > Omitting `tools:` from a task means `:none`: the robot sends no tools at all.
303
+ > Use `tools: :inherit` for "send everything attached".
304
+
264
305
  The `Task` wrapper deep-merges per-task context with the network's run params before delegating to the robot's `call` method.
265
306
 
266
307
  ## Pipeline Pattern
@@ -298,13 +339,19 @@ puts result.value.last_text_content
298
339
  Fan-out / fan-in pattern where multiple robots analyze in parallel and a synthesizer merges results:
299
340
 
300
341
  ```ruby
342
+ # Robot names deliberately match their task names -- result.context is keyed
343
+ # by the ROBOT's name, so mismatched names produce nil lookups below.
344
+ sentiment = RobotLab.build(name: "sentiment", system_prompt: "Score sentiment.")
345
+ entities = RobotLab.build(name: "entities", system_prompt: "Extract entities.")
346
+ keywords = RobotLab.build(name: "keywords", system_prompt: "Extract keywords.")
347
+
301
348
  network = RobotLab.create_network(name: "multi_analysis") do
302
349
  task :prepare, preparer, depends_on: :none
303
350
 
304
351
  # These run in parallel (all depend on :prepare)
305
- task :sentiment, sentiment_analyzer, depends_on: [:prepare]
306
- task :entities, entity_extractor, depends_on: [:prepare]
307
- task :keywords, keyword_extractor, depends_on: [:prepare]
352
+ task :sentiment, sentiment, depends_on: [:prepare]
353
+ task :entities, entities, depends_on: [:prepare]
354
+ task :keywords, keywords, depends_on: [:prepare]
308
355
 
309
356
  # Waits for all three to complete
310
357
  task :summarize, summarizer, depends_on: [:sentiment, :entities, :keywords]
@@ -312,13 +359,19 @@ end
312
359
 
313
360
  result = network.run(message: "Analyze this text")
314
361
 
315
- # Access parallel results from context
362
+ # Access parallel results from context (keyed by robot name)
316
363
  puts "Sentiment: #{result.context[:sentiment].last_text_content}"
317
364
  puts "Entities: #{result.context[:entities].last_text_content}"
318
365
  puts "Keywords: #{result.context[:keywords].last_text_content}"
319
366
  puts "Summary: #{result.value.last_text_content}"
320
367
  ```
321
368
 
369
+ > [!WARNING]
370
+ > `result.context` is keyed by `@name` of the robot that wrote it, not by the task
371
+ > name. Had these robots kept names like `sentiment_analyzer`, the lookups above
372
+ > would return `nil` even though every task ran. Keep robot name and task name
373
+ > identical.
374
+
322
375
  ## Shared Memory in Networks
323
376
 
324
377
  Networks provide a shared `Memory` instance that all robots can read and write. This is especially useful for parallel robots that need to coordinate:
@@ -374,30 +427,47 @@ network = RobotLab.create_network(name: "validated_pipeline") do
374
427
  end
375
428
 
376
429
  result = network.run(message: "Process this")
377
- if result.halted?
378
- puts "Validation failed: #{result.value.last_text_content}"
379
- else
430
+ if result.continue?
380
431
  puts "Processing complete: #{result.value.last_text_content}"
432
+ else
433
+ puts "Validation failed: #{result.value.last_text_content}"
381
434
  end
382
435
  ```
383
436
 
437
+ > [!WARNING]
438
+ > `SimpleFlow::Result` has **no** `halted?` or `continued?` predicate — calling
439
+ > either raises `NoMethodError`. The predicate is `continue?`, which returns
440
+ > `false` after `halt`. The full public API is exactly `activate`,
441
+ > `activated_steps`, `context`, `continue`, `continue?`, `errors`, `halt`, `value`,
442
+ > `with_context`, and `with_error`. `with_value` is **private** — calling it raises
443
+ > `NoMethodError`; use `continue(value)` or `halt(value)` to set the value.
444
+
384
445
  ## Running
385
446
 
386
447
  ```bash
387
448
  export ANTHROPIC_API_KEY="your-key"
388
- ruby examples/customer_service.rb
449
+
450
+ # Classifier + three specialists with optional-task routing
451
+ ruby examples/03_network.rb
452
+
453
+ # Shared reactive memory across concurrent robots
454
+ ruby examples/07_network_memory.rb
455
+
456
+ # Network visualization and introspection (no LLM calls)
457
+ ruby examples/11_network_introspection.rb
389
458
  ```
390
459
 
391
460
  ## Key Concepts
392
461
 
393
- 1. **SimpleFlow Pipeline**: DAG-based execution with dependency management via `depends_on:`
462
+ 1. **SimpleFlow Pipeline**: DAG-based execution with dependency management via `depends_on:`. The DSL method is `task` — there is no `step`, no `router:` kwarg, and no `Router` class
394
463
  2. **Optional Tasks**: Use `depends_on: :optional` for tasks activated dynamically by classifiers
395
- 3. **Robot#call Override**: Custom routing logic in classifier robots that override the `call` method
464
+ 3. **Robot#call Override**: Routing is written in Ruby subclass `Robot`, override `call`, and call `result.activate(:task_name)`
396
465
  4. **extract_run_context**: Helper method to extract message and params from `SimpleFlow::Result`
397
- 5. **Context Flow**: Data passed through `result.context` and accessed by downstream robots
398
- 6. **Parallel Execution**: Tasks with the same dependencies run concurrently
399
- 7. **Shared Memory**: Network memory (`network_memory:`) enables inter-robot communication
400
- 8. **Per-Task Configuration**: Each task can have its own context, tools, and MCP servers via `Task`
466
+ 5. **Context Flow**: `result.context` is keyed by the **robot's** name (`with_context(@name.to_sym, ...)`), while `activate` takes a **task** name — keep the two identical
467
+ 6. **Result predicates**: `continue?` (not `halted?`), plus `activated_steps` to see what routing actually did
468
+ 7. **Parallel Execution**: Tasks with the same dependencies run concurrently; cap in-flight LLM calls with `RunConfig.new(max_concurrent_robots: N)`
469
+ 8. **Shared Memory**: Network memory (`network_memory:`) enables inter-robot communication
470
+ 9. **Per-Task Configuration**: Each task can carry its own context, MCP servers, and a `tools:` allowlist over the robot's already-attached tools
401
471
 
402
472
  ## See Also
403
473
 
@@ -6,13 +6,36 @@ Robots with external capabilities through tools.
6
6
 
7
7
  This example demonstrates how to give robots access to external systems through tools. Tools are defined as `RubyLLM::Tool` subclasses or `RobotLab::Tool` instances and passed to robots via the `local_tools:` parameter.
8
8
 
9
+ > [!WARNING]
10
+ > **Attaching a tool is not the same as sending it.** `Robot#run` defaults to
11
+ > `tools: :none`, so a plain `robot.run("...")` sends the LLM zero tools even when
12
+ > `local_tools:` were supplied at build time. Pass `tools: :inherit` on every call
13
+ > that should be able to use them:
14
+ >
15
+ > ```ruby
16
+ > robot.run("What is 15 * 7?", tools: :inherit)
17
+ > ```
18
+ >
19
+ > For a **standalone** robot, do not pass `tools: :inherit` in the constructor:
20
+ > at build time it resolves against the parent level (`:none`) and yields an
21
+ > allowlist matching nothing, suppressing the tools even when the run asks for
22
+ > them. Leave `tools:` unset there.
23
+ >
24
+ > Inside a **network**, the opposite holds — build-time `tools: :inherit` is how a
25
+ > robot opts into the network `config:`'s allowlist. See
26
+ > [Creating Networks](../guides/creating-networks.md).
27
+ >
28
+ > An explicit array (`tools: [Calculator]`) is an allowlist, not a local-vs-MCP
29
+ > switch, and its entries must match how each tool was attached — this page
30
+ > attaches classes, so use class names.
31
+
9
32
  ## RubyLLM::Tool Subclass Pattern
10
33
 
11
34
  The primary way to define tools is by subclassing `RubyLLM::Tool`:
12
35
 
13
36
  ```ruby
14
37
  #!/usr/bin/env ruby
15
- # examples/tool_usage.rb
38
+ # Tool definitions mirror examples/02_tools.rb
16
39
 
17
40
  require "bundler/setup"
18
41
  require "robot_lab"
@@ -79,21 +102,26 @@ robot = RobotLab.build(
79
102
  model: "claude-sonnet-4"
80
103
  )
81
104
 
82
- # Run the robot
83
- result = robot.run("What is 15 multiplied by 7? Also, give me a career fortune.")
105
+ # Run the robot -- tools: :inherit is what actually sends Calculator and
106
+ # FortuneCookie to the LLM
107
+ result = robot.run(
108
+ "What is 15 multiplied by 7? Also, give me a career fortune.",
109
+ tools: :inherit
110
+ )
84
111
 
85
112
  # Display results
86
113
  puts "Response: #{result.last_text_content}"
87
114
 
88
- if result.tool_calls.any?
89
- puts "\nTool calls made:"
90
- result.tool_calls.each do |tc|
91
- tool_info = tc.respond_to?(:tool) ? tc.tool : tc
92
- puts " #{tool_info[:name] || tool_info}"
93
- end
94
- end
115
+ # Confirm which tools were sent for this turn
116
+ puts "Tools sent: #{robot.chat.tools.keys.join(', ')}"
95
117
  ```
96
118
 
119
+ > [!NOTE]
120
+ > `result.tool_calls` is effectively always empty. It reads the *final* assistant
121
+ > message, and by the time ruby_llm's tool loop has finished that message contains
122
+ > only text. To observe tool activity, use the `on_tool_call:` callback (below) or
123
+ > the `:tool_call` hooks.
124
+
97
125
  ## RobotLab::Tool.create Pattern
98
126
 
99
127
  For simpler tools that do not need their own class, use `RobotLab::Tool.create`:
@@ -127,15 +155,22 @@ robot = RobotLab.build(
127
155
  model: "claude-sonnet-4"
128
156
  )
129
157
 
130
- result = robot.run("What time is it and what's the weather in New York?")
158
+ result = robot.run("What time is it and what's the weather in New York?", tools: :inherit)
131
159
  puts result.last_text_content
132
160
  ```
133
161
 
162
+ The block receives a single hash of symbol-keyed arguments.
163
+
164
+ > [!NOTE]
165
+ > `RobotLab::Tool.create(parameters:)` accepts a JSON-Schema-shaped hash, but only
166
+ > `type` and `description` are read off each property, plus the top-level `required`
167
+ > list. Anything else (`enum`, `default`, nested `items`) is ignored — ruby_llm's
168
+ > `param` DSL supports only `type:`, `desc:`/`description:`, and `required:`.
169
+
134
170
  ## Weather API Integration
135
171
 
136
172
  ```ruby
137
173
  #!/usr/bin/env ruby
138
- # examples/weather_assistant.rb
139
174
 
140
175
  require "bundler/setup"
141
176
  require "robot_lab"
@@ -227,7 +262,7 @@ loop do
227
262
  break if input.nil? || input.downcase == "quit"
228
263
  next if input.empty?
229
264
 
230
- result = weather_bot.run(input)
265
+ result = weather_bot.run(input, tools: :inherit)
231
266
  puts "\nAssistant: #{result.last_text_content}"
232
267
  end
233
268
 
@@ -237,8 +272,6 @@ puts "\nGoodbye!"
237
272
  ## Database Integration
238
273
 
239
274
  ```ruby
240
- # examples/order_assistant.rb
241
-
242
275
  require "robot_lab"
243
276
 
244
277
  # Mock database
@@ -297,7 +330,7 @@ order_bot = RobotLab.build(
297
330
  )
298
331
 
299
332
  # Run with a question
300
- result = order_bot.run("What's the status of order ORD001?")
333
+ result = order_bot.run("What's the status of order ORD001?", tools: :inherit)
301
334
  puts result.last_text_content
302
335
  ```
303
336
 
@@ -314,27 +347,41 @@ robot = RobotLab.build(
314
347
  on_tool_call: ->(tool_call) {
315
348
  puts "[Tool Call] #{tool_call.name}: #{tool_call.arguments}"
316
349
  },
317
- on_tool_result: ->(tool_call, result) {
318
- puts "[Tool Result] #{tool_call.name}: #{result}"
350
+ # NOTE: one argument -- the result. There is no tool_call parameter here.
351
+ on_tool_result: ->(result) {
352
+ puts "[Tool Result] #{result}"
319
353
  }
320
354
  )
321
355
 
322
- result = robot.run("What is 42 * 17?")
356
+ result = robot.run("What is 42 * 17?", tools: :inherit)
323
357
  ```
324
358
 
359
+ > [!WARNING]
360
+ > `on_tool_result` receives **exactly one** argument, the tool's return value.
361
+ > Writing `->(tool_call, result)` raises
362
+ > `ArgumentError: wrong number of arguments (given 1, expected 2)` the first time a
363
+ > tool runs.
364
+ >
365
+ > Both callbacks map onto ruby_llm's legacy hooks and are deprecated as of
366
+ > ruby_llm 1.16 — wiring either one emits
367
+ > ``` `on_tool_call` is deprecated and will be removed in RubyLLM 2.0. Use `before_tool_call` instead. ```
368
+ > The additive replacements are `before_message`, `after_message`,
369
+ > `before_tool_call`, and `after_tool_result`. RobotLab's own
370
+ > [Hook system](../guides/hooks.md) is the supported way to observe tool activity.
371
+
325
372
  ## Running
326
373
 
327
374
  ```bash
328
375
  export ANTHROPIC_API_KEY="your-key"
329
376
 
330
- # Tool usage example
331
- ruby examples/tool_usage.rb
377
+ # Tool definitions and a robot that uses them
378
+ ruby examples/02_tools.rb
332
379
 
333
- # Weather assistant
334
- ruby examples/weather_assistant.rb
380
+ # Tool loop circuit breaker (max_tool_rounds)
381
+ ruby examples/20_circuit_breaker.rb
335
382
 
336
- # Order lookup
337
- ruby examples/order_assistant.rb
383
+ # Ractor-safe CPU tools (no LLM calls)
384
+ ruby examples/29_ractor_tools.rb
338
385
  ```
339
386
 
340
387
  ## Interactive User Input
@@ -348,17 +395,30 @@ robot = RobotLab.build(
348
395
  name: "interviewer",
349
396
  system_prompt: <<~PROMPT,
350
397
  You are a project setup assistant. Interview the user to understand their
351
- needs, then summarize the project plan. Use the ask_user tool to gather
352
- information one question at a time.
398
+ needs, then summarize the project plan. Use the robot_lab--ask_user tool to
399
+ gather information one question at a time.
353
400
  PROMPT
354
- local_tools: [RobotLab::AskUser],
355
401
  model: "claude-sonnet-4"
356
402
  )
357
403
 
358
- result = robot.run("Help me plan a new web application")
404
+ # Attach an INSTANCE bound to the robot so the tool uses robot.input/robot.output
405
+ robot.local_tools << RobotLab::AskUser.new(robot: robot)
406
+
407
+ result = robot.run("Help me plan a new web application", tools: :inherit)
359
408
  puts "\nProject Plan:\n#{result.last_text_content}"
360
409
  ```
361
410
 
411
+ > [!WARNING]
412
+ > Pass an **instance**, not the class. `RobotLab::Tool` derives its LLM-visible
413
+ > name from the full class name, so `RobotLab::AskUser` is presented to the model
414
+ > as **`robot_lab--ask_user`**, not `ask_user` — name it that way in your system
415
+ > prompt.
416
+ >
417
+ > An instance created without `robot:` has `tool.robot == nil`, so it ignores
418
+ > `robot.input` / `robot.output`, falls back to `$stdin` / `$stdout`, and labels
419
+ > every prompt `[Robot]` instead of the robot's name. Always use
420
+ > `RobotLab::AskUser.new(robot: robot)`.
421
+
362
422
  The robot will ask questions interactively:
363
423
 
364
424
  ```
@@ -375,24 +435,31 @@ The robot will ask questions interactively:
375
435
  > Customer support portal
376
436
  ```
377
437
 
378
- For testing, inject `StringIO` objects:
438
+ For testing, inject `StringIO` objects on the robot *before* building the tool
439
+ instance (the tool reads them through its `robot` reference at call time):
379
440
 
380
441
  ```ruby
381
442
  robot.input = StringIO.new("Ruby\nyes\nCustomer portal\n")
382
443
  robot.output = StringIO.new
383
444
  ```
384
445
 
446
+ `RobotLab::AskUser` can also be driven directly, outside an LLM turn — see
447
+ [`examples/06_prompt_templates.rb`](https://github.com/MadBomber/robot_lab/blob/main/examples/06_prompt_templates.rb),
448
+ which calls `RobotLab::AskUser.new.call("question" => ..., "default" => ...)` to
449
+ collect a value before any robot is built.
450
+
385
451
  ## Key Concepts
386
452
 
387
453
  1. **RubyLLM::Tool subclass**: Define a class with `description`, `param`, and `execute` method
388
- 2. **RobotLab::Tool subclass**: Same DSL plus `robot` accessor for robot-aware tools
454
+ 2. **RobotLab::Tool subclass**: Same DSL plus a `robot` accessor for robot-aware tools
389
455
  3. **RobotLab::Tool.create**: Use `RobotLab::Tool.create(name:, description:, &block)` for dynamic tools
390
- 4. **Built-in tools**: `RobotLab::AskUser` for interactive terminal input
391
- 5. **local_tools**: Pass tool classes/instances via `local_tools:` parameter to `RobotLab.build` or `Robot.new`
392
- 6. **Frontmatter tools**: Declare tool class names in template YAML front matter (`tools: [Calculator]`) for self-contained templates
393
- 7. **Error Handling**: Return error hashes (e.g., `{ error: "message" }`) for graceful failures
394
- 8. **Callbacks**: Use `on_tool_call:` and `on_tool_result:` for monitoring
395
- 9. **Result Access**: Check `result.tool_calls` for tool call history, `result.last_text_content` for the final response
456
+ 4. **Built-in tools**: `RobotLab::AskUser` (LLM-visible name `robot_lab--ask_user`) for interactive terminal input
457
+ 5. **local_tools**: Pass tool classes/instances via `local_tools:` to `RobotLab.build` or `Robot.new` — this *attaches* them
458
+ 6. **tools: :inherit**: Required on `run` to actually *send* the attached tools; the default is `tools: :none`
459
+ 7. **Frontmatter tools**: Declare tool class names in template YAML front matter (`tools: [Calculator]`); they populate `local_tools`, and still need `tools: :inherit` at run time. Constructor `local_tools:` overrides the front-matter list
460
+ 8. **Error Handling**: Raised exceptions are caught and returned to the LLM as text (`"Error (tool_name): message"`); a `RobotLab::ToolError` with `retryable: true` appends `" (retryable)"`. Set `self.raise_on_error = true` on a class to opt out — note this is per-class and is *not* inherited by subclasses
461
+ 9. **Tool cap**: At most `DEFAULT_MAX_TOOLS` (128) tools are sent. Setting `max_tools` to nil, 0, or a negative number falls back to 128 — the cap cannot be disabled
462
+ 10. **Result Access**: Use `result.last_text_content` (alias `result.reply`) for the final response. `result.tool_calls` is effectively always empty — observe tool activity with `on_tool_call:` or the Hook system instead
396
463
 
397
464
  ## See Also
398
465