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
@@ -84,6 +84,22 @@ network = RobotLab.create_network(name: "parallel_analysis") do
84
84
  end
85
85
  ```
86
86
 
87
+ This shared-`depends_on` form is the supported way to fan out.
88
+
89
+ ### The `parallel` Block
90
+
91
+ `Network#parallel(name = nil, depends_on: :none, &block)` forwards to the underlying SimpleFlow pipeline's parallel group.
92
+
93
+ > [!WARNING]
94
+ > **`parallel` cannot currently register robot tasks.** Its block is
95
+ > `instance_eval`'d by `SimpleFlow::Pipeline::ParallelBlock`, which defines
96
+ > `step`, not `task` — writing `task :a, robot` inside raises
97
+ > `NoMethodError: undefined method 'task' for an instance of
98
+ > SimpleFlow::Pipeline::ParallelBlock`. And `step` bypasses `Network#task`, so
99
+ > the robot is never added to `network.robots`, never wired to the bus poller,
100
+ > and never wrapped in a `Task`. Use the shared-`depends_on` form above instead;
101
+ > it produces the same parallel execution through the supported path.
102
+
87
103
  ### Concurrency Cap
88
104
 
89
105
  When a network fans out to many parallel robots, each makes a simultaneous LLM API call. With no limit this can exhaust API rate-limit quotas or database connection pools under load. Set `max_concurrent_robots:` on a `RunConfig` to cap how many robot tasks run at once — the rest queue behind an `Async::Semaphore` and start as slots open:
@@ -105,7 +121,15 @@ network = RobotLab.create_network(name: "launch_assessment", config: config) do
105
121
  end
106
122
  ```
107
123
 
108
- `nil` (the default) means unlimited — identical to pre-existing behavior. For Rails deployments, size the cap to match your database connection pool and API rate tier. See [Example 31](../../examples/31_launch_assessment.rb) for a working demo.
124
+ `nil` (the default) means unlimited — identical to pre-existing behavior. For Rails deployments, size the cap to match your database connection pool and API rate tier. See [examples/31_launch_assessment.rb](https://github.com/MadBomber/robot_lab/blob/main/examples/31_launch_assessment.rb) for a working demo.
125
+
126
+ > [!WARNING]
127
+ > The cap is enforced by an `Async::Semaphore`, so it only applies on the **async
128
+ > execution path** — `concurrency: :async`, or `concurrency: :auto` when the
129
+ > `async` gem is available (both the default). With
130
+ > `concurrency: :threads` the value is accepted but **ignored**: every parallel
131
+ > task starts immediately. Do not rely on `max_concurrent_robots:` for rate
132
+ > limiting in a thread-mode network.
109
133
 
110
134
  ### Optional Tasks
111
135
 
@@ -125,6 +149,8 @@ end
125
149
  Tasks can have individual context and configuration that is deep-merged with the network's run parameters:
126
150
 
127
151
  ```ruby
152
+ # technical_robot was built with local_tools: [DebugTool, LogTool, TraceTool] —
153
+ # attached as classes, so the task's allowlist names them as classes too.
128
154
  network = RobotLab.create_network(name: "support") do
129
155
  task :classifier, classifier_robot, depends_on: :none
130
156
  task :billing, billing_robot,
@@ -149,6 +175,42 @@ end
149
175
  | `depends_on` | `:none`, `[:task1]`, or `:optional` |
150
176
  | `poller_group` | Bus delivery group label (`:default`, `:slow`, etc.) |
151
177
 
178
+ > [!IMPORTANT]
179
+ > `tools:` and `mcp:` both default to `:none`, and `:none` means "send zero tools
180
+ > this turn" — not "fall back to whatever the robot was built with". A task that
181
+ > omits `tools:` runs its robot with **no tools at all**, even if the robot was
182
+ > constructed with `local_tools:`. Write `tools: :inherit` on the task to send the
183
+ > robot's attached tools.
184
+ >
185
+ > One exception: a non-`:none` `tools:`/`mcp:` set by an **earlier** task carries
186
+ > forward through the shared run params to later tasks — see
187
+ > [Network-Wide Tool and MCP Defaults](#network-wide-tool-and-mcp-defaults).
188
+ >
189
+ > An explicit array is an **allowlist over the tools the robot already has** —
190
+ > it selects, it does not add. `tools: [DebugTool, LogTool]` on a robot built
191
+ > without those tools resolves to an empty set. Attach the tools with
192
+ > `local_tools:` at build time and use the task's `tools:` to narrow.
193
+ >
194
+ > **Entries must match how the tool was attached.** The filter compares each
195
+ > entry against `tool.name`, and `Class#name` is not `RubyLLM::Tool#name`. A tool
196
+ > attached as a **class** (`local_tools: [DebugTool]`) is named `"DebugTool"`; the
197
+ > same tool attached as an **instance** (`local_tools: [DebugTool.new]`) is named
198
+ > `"debug"`. Verified:
199
+ >
200
+ > | attached as | allowlist entry | result |
201
+ > |---|---|---|
202
+ > | class | `[DebugTool]` | matches |
203
+ > | class | `%w[debug]` | no match |
204
+ > | instance | `%w[debug]` | matches |
205
+ > | instance | `[DebugTool]` | no match |
206
+ >
207
+ > Mixing the two forms is the usual cause of a task that silently ends up with
208
+ > zero tools. `tools: :inherit` sidesteps the question entirely — it sends every
209
+ > attached tool without filtering.
210
+ >
211
+ > MCP needs both values: `mcp: :inherit` triggers the connection, and
212
+ > `tools: :inherit` is additionally required for the MCP tools to reach the model.
213
+
152
214
  ## Conditional Routing
153
215
 
154
216
  Use optional tasks with custom Robot subclasses for intelligent routing:
@@ -192,6 +254,13 @@ end
192
254
 
193
255
  Each network maintains a shared `BusPoller` that serializes TypedBus deliveries on a per-robot basis: if a robot is already processing a message, new deliveries are queued and drained after the current one completes. This prevents re-entrancy without blocking other robots.
194
256
 
257
+ > [!NOTE]
258
+ > Despite the name, `BusPoller` runs **no background thread**. `start` and `stop`
259
+ > are no-ops, `running?` is hard-coded `true`, and `enqueue` processes and drains
260
+ > inline in the caller's own execution context (Async fiber or OS thread). All it
261
+ > owns is a mutex plus a per-robot queue. Don't expect deliveries to make
262
+ > progress on their own while the calling fiber is parked.
263
+
195
264
  Named **poller groups** let you label tasks so slow robots are identifiable in logs and monitoring without needing separate infrastructure:
196
265
 
197
266
  ```ruby
@@ -234,7 +303,7 @@ result = network.run(
234
303
  ```ruby
235
304
  result = network.run(message: "Process this")
236
305
 
237
- # Access individual robot results
306
+ # Access individual robot results — keyed by the ROBOT's name
238
307
  classifier_result = result.context[:classifier]
239
308
  billing_result = result.context[:billing]
240
309
 
@@ -242,6 +311,14 @@ billing_result = result.context[:billing]
242
311
  original_params = result.context[:run_params]
243
312
  ```
244
313
 
314
+ > [!IMPORTANT]
315
+ > `result.context` is keyed by the **robot's `name:`**, not by the task label.
316
+ > The default `Robot#call` writes `result.with_context(@name.to_sym, robot_result)`.
317
+ > The lookups above only work because each task label matches its robot's name —
318
+ > `task :classifier, RobotLab.build(name: "classifier", ...)`. If they differ, you
319
+ > must index by the robot name. Keeping the two identical is the simplest way to
320
+ > avoid the mismatch. (`network.robots`, by contrast, *is* keyed by task name.)
321
+
245
322
  ## SimpleFlow::Result
246
323
 
247
324
  Networks return a `SimpleFlow::Result` object:
@@ -249,12 +326,24 @@ Networks return a `SimpleFlow::Result` object:
249
326
  ```ruby
250
327
  result = network.run(message: "Hello")
251
328
 
252
- result.value # The final task's output (RobotResult)
253
- result.context # Hash of all task results and metadata
254
- result.halted? # Whether execution was halted early
255
- result.continued? # Whether execution continued normally
329
+ result.value # The final task's output (RobotResult)
330
+ result.context # Hash of all robot results and metadata
331
+ result.continue? # true while execution is proceeding; false once a robot called halt
332
+ result.activated_steps # Symbols of the :optional tasks that were activated
333
+ result.errors # Accumulated errors
256
334
  ```
257
335
 
336
+ The full public API is exactly `activate`, `activated_steps`, `context`, `continue`, `continue?`, `errors`, `halt`, `value`, `with_context`, `with_error`.
337
+
338
+ > [!WARNING]
339
+ > `result.halted?` and `result.continued?` **do not exist** — calling either
340
+ > raises `NoMethodError`. There is a single predicate, `continue?`; a halted
341
+ > result is simply `continue? == false`.
342
+ >
343
+ > `result.with_value` is **private** (`public_method_defined?(:with_value)` is
344
+ > `false`), so calling it from a robot raises `NoMethodError` too. Use
345
+ > `continue(new_value)` to carry a new value forward.
346
+
258
347
  ## Broadcasting
259
348
 
260
349
  Networks support a broadcast channel for network-wide announcements:
@@ -341,7 +430,7 @@ When a robot raises an exception during pipeline execution, the error is caught
341
430
 
342
431
  ```ruby
343
432
  # If billing_robot raises an error, the network continues
344
- # The error is available in the result context:
433
+ # The error is available in the result context, keyed by the robot's name:
345
434
  result = network.run(message: "Process this")
346
435
  billing_result = result.context[:billing]
347
436
 
@@ -384,7 +473,7 @@ Access previous task results via context:
384
473
  ```ruby
385
474
  class ResponderRobot < RobotLab::Robot
386
475
  def call(result)
387
- # Get classifier's output
476
+ # Get classifier's output — the key is the classifier ROBOT's name
388
477
  classification = result.context[:classifier]&.last_text_content
389
478
 
390
479
  context = extract_run_context(result)
@@ -435,61 +524,161 @@ puts network.execution_plan
435
524
 
436
525
  ```ruby
437
526
  network.name # => "support"
438
- network.robots # => Hash of name => Robot
527
+ network.robots # => Hash of key => Robot (see the key caveat below)
439
528
  network.robot(:billing) # => Robot instance
440
529
  network["billing"] # => Robot instance (alias)
441
530
  network.available_robots # => Array of Robot instances
531
+ network.crew # => Array of Robot instances (Runnable protocol)
442
532
  network.memory # => Memory instance (shared)
443
533
  network.to_h # => Hash representation
534
+
535
+ network.add_robot(extra_robot) # add without a pipeline task -> self
536
+ network.remove_robot(:extra_robot) # remove by name -> the removed Robot, or nil
444
537
  ```
445
538
 
539
+ > [!WARNING]
540
+ > `network.robots` uses **two different key conventions**. `task :alpha, bot`
541
+ > registers the robot under the **task** name (`"alpha"`), while
542
+ > `network.add_robot(bot)` registers it under **`bot.name`**. `network.robot(...)`
543
+ > and `network[...]` inherit the same split. When task label and robot name
544
+ > differ, a lookup by robot name will miss a task-registered robot.
545
+ >
546
+ > `add_robot` also raises `ArgumentError` if the key is already taken:
547
+ > `Robot 'x' already exists in network 'n'`. Because the key conventions differ,
548
+ > adding a robot whose `name` matches an existing *task label* collides, while
549
+ > adding one whose name matches a task-registered robot's `name` does not.
550
+
551
+ `remove_robot` only drops the robot from the crew — it doesn't touch the pipeline, so don't remove a robot that's still a `depends_on` target of a task.
552
+
446
553
  ## Configuration Inheritance
447
554
 
448
- Networks accept a `config:` parameter that establishes default LLM settings for all member robots. This is useful when you want consistent behavior across a pipeline without configuring each robot individually.
555
+ Networks accept a `config:` parameter, but its reach is much narrower than a general "network-wide defaults" mechanism.
556
+
557
+ > [!WARNING]
558
+ > A network-level (or per-task) `RunConfig` propagates **only `mcp` and `tools`**
559
+ > to member robots, and only for robots that opt in with `:inherit`. LLM fields
560
+ > (`model`, `temperature`, `top_p`, `max_tokens`, …) and callbacks (`on_content`,
561
+ > `on_tool_call`, `on_tool_result`) are **never** inherited from a network —
562
+ > each robot reads those from its own config at construction time. Setting
563
+ > `model:` on a network config has no effect on any robot.
564
+ >
565
+ > Verified: a robot in a network configured with
566
+ > `RunConfig.new(model: "claude-haiku-4-5-20251001", temperature: 0.11)` still runs
567
+ > with the global default model and a `nil` temperature.
449
568
 
450
- ### Network-Wide Defaults
569
+ The one field the network consumes for itself is `max_concurrent_robots` (see [Concurrency Cap](#concurrency-cap)).
570
+
571
+ ### Network-Wide Tool and MCP Defaults
572
+
573
+ Reaching a robot from a network `config:` takes **two** opt-ins, because
574
+ resolution runs in two passes — first the robot's build-time value against the
575
+ network config, then the task's runtime value against that result:
576
+
577
+ 1. the robot is built with `tools: :inherit` (and/or `mcp: :inherit`), which is
578
+ what pulls the network config's list down to the robot level, and
579
+ 2. the task passes `tools: :inherit` (and/or `mcp: :inherit`), which is what
580
+ actually sends them for that turn.
451
581
 
452
582
  ```ruby
453
- # All robots in this network use the same model and temperature
454
- shared = RobotLab::RunConfig.new(model: "claude-sonnet-4", temperature: 0.5)
583
+ shared = RobotLab::RunConfig.new(
584
+ tools: [SearchTool, CalculatorTool],
585
+ mcp: [{ name: "fs", transport: { type: "stdio", command: "mcp-server-filesystem" } }]
586
+ )
587
+
588
+ # Build-time :inherit is the opt-in to `shared`. The robot must still *attach*
589
+ # the tools — the network list narrows what it already has, it cannot add.
590
+ analyzer_robot = RobotLab.build(
591
+ name: "analyzer",
592
+ system_prompt: "...",
593
+ local_tools: [SearchTool, CalculatorTool, DraftTool],
594
+ tools: :inherit,
595
+ mcp: :inherit
596
+ )
455
597
 
456
598
  network = RobotLab.create_network(name: "pipeline", config: shared) do
457
- task :analyzer, analyzer_robot, depends_on: :none
458
- task :writer, writer_robot, depends_on: [:analyzer]
459
- task :reviewer, reviewer_robot, depends_on: [:writer]
599
+ task :analyzer, analyzer_robot, depends_on: :none, tools: :inherit, mcp: :inherit
600
+ task :writer, writer_robot, depends_on: [:analyzer]
460
601
  end
461
602
  ```
462
603
 
463
- ### Per-Task Overrides
604
+ Verified outcomes for the `:analyzer` task above:
605
+
606
+ | robot build `tools:` | task `tools:` | tools sent |
607
+ |---|---|---|
608
+ | `:inherit` | `:inherit` | `[:search, :calculator]` — `shared` applied, `DraftTool` filtered out |
609
+ | unset | `:inherit` | `[:search, :calculator, :draft]` — `shared` never reached the robot |
610
+ | `:inherit` | omitted (`:none`) | `[]` |
611
+
612
+ Same for MCP: with `mcp: :inherit` at build time the `fs` server connection is
613
+ attempted (it shows up in `robot.failed_mcp_server_names` if it fails); without
614
+ it, the network's `mcp:` list never reaches the robot and nothing is attempted.
615
+
616
+ > [!IMPORTANT]
617
+ > A task's `tools:`/`mcp:` **carries forward to later tasks**. `Task` writes them
618
+ > into the shared `run_params`, and the next task deep-merges that hash as its
619
+ > base — so `:writer` above ends up with `tools: :inherit` too, inherited from
620
+ > `:analyzer`, even though its own `task` line says nothing about tools. Verified:
621
+ > put `:writer` *first* in the pipeline and it resolves to `[]`; put it after an
622
+ > `:analyzer` that passes `tools: :inherit` and it resolves to
623
+ > `[:search, :calculator]`.
624
+ >
625
+ > Writing `tools: :none` on the downstream task does **not** undo it — `Task`
626
+ > skips writing `:none` into `run_params` (`run_params[:tools] = @tools unless
627
+ > @tools == :none`), so the inherited value survives untouched. Use an empty
628
+ > array instead, which *is* written and is treated as an explicit zero:
629
+ >
630
+ > ```ruby
631
+ > task :writer, writer_robot, depends_on: [:analyzer], tools: [] # verified -> []
632
+ > task :writer, writer_robot, depends_on: [:analyzer], tools: :none # verified -> still [:search, :calculator]
633
+ > ```
634
+
635
+ > [!NOTE]
636
+ > This is the one place where build-time `:inherit` is correct. For a
637
+ > **standalone** robot the parent level is the global `:none`, so build-time
638
+ > `:inherit` there resolves to an allowlist of `["none"]` and matches nothing —
639
+ > see [Building Robots: Adding Tools](building-robots.md#adding-tools).
464
640
 
465
- Individual tasks can override the network's config with their own `config:`:
641
+ To give every robot the same model, set it on each robot at construction:
466
642
 
467
643
  ```ruby
468
- creative_config = RobotLab::RunConfig.new(temperature: 0.9)
644
+ MODEL = "claude-sonnet-4"
469
645
 
646
+ analyzer_robot = RobotLab.build(name: "analyzer", system_prompt: "...", model: MODEL, temperature: 0.5)
647
+ writer_robot = RobotLab.build(name: "writer", system_prompt: "...", model: MODEL, temperature: 0.5)
648
+ ```
649
+
650
+ ### Per-Task `config:`
651
+
652
+ A per-task `config:` is merged into the network config that reaches the robot, so it carries the same `mcp`/`tools`-only limitation:
653
+
654
+ ```ruby
655
+ # writer_robot built with local_tools: [SearchTool, CalculatorTool, DraftTool]
656
+ # and tools: :inherit — the same two opt-ins as above.
470
657
  network = RobotLab.create_network(name: "pipeline", config: shared) do
471
- task :analyzer, analyzer_robot, depends_on: :none
658
+ task :analyzer, analyzer_robot, depends_on: :none, tools: :inherit
472
659
  task :writer, writer_robot,
473
- config: creative_config, # writer gets higher temperature
660
+ config: RobotLab::RunConfig.new(tools: [DraftTool]), # tools/mcp only
661
+ tools: :inherit,
474
662
  depends_on: [:analyzer]
475
- task :reviewer, reviewer_robot, depends_on: [:writer]
476
663
  end
477
664
  ```
478
665
 
666
+ The task `config:` replaces the network's list for that task, so `:writer`
667
+ resolves to `[:draft]` while `:analyzer` resolves to `[:search, :calculator]`.
668
+
479
669
  ### Inheritance Chain
480
670
 
481
- The full configuration hierarchy (most-specific wins):
671
+ Per robot, the cascade runs least- to most-specific — note that template front matter is the **base**, not an override:
482
672
 
483
673
  ```
484
- RobotLab.config (global)
485
- -> Network config
486
- -> Task config
487
- -> Robot config (from constructor)
488
- -> Template front matter
489
- -> Constructor kwargs (model:, temperature:, etc.)
674
+ template front matter (lowest — the starting point)
675
+ -> config: RunConfig
676
+ -> constructor kwargs (highest — always wins)
490
677
  ```
491
678
 
492
- Each layer only overrides values it explicitly sets. Unset values pass through from the parent.
679
+ Constructor kwargs always win. Network and task `config:` sit *outside* this chain and contribute only `mcp`/`tools`, and only where a robot or task asks for `:inherit`.
680
+
681
+ Two further front-matter caveats: only `model` and `temperature` are actually applied from front matter — `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, and `stop` are parsed and silently dropped. The same six *do* work as constructor kwargs.
493
682
 
494
683
  ## Best Practices
495
684
 
@@ -522,7 +711,7 @@ Guard against missing optional task results:
522
711
 
523
712
  ```ruby
524
713
  def call(result)
525
- # Check if optional task ran
714
+ # Check if the optional task ran (key = that robot's name)
526
715
  if result.context[:validator]
527
716
  # Use validator result
528
717
  else