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
@@ -19,6 +19,10 @@ network = RobotLab.create_network(name: "customer_service") do
19
19
  end
20
20
  ```
21
21
 
22
+ ## Runnable Protocol
23
+
24
+ `Network` implements `RobotLab::Runnable` — the same interface `Robot` implements — so code that needs to run "a robot or a network" doesn't have to branch on `is_a?(RobotLab::Network)`. For a `Network`: `crew` returns `robots.values` (pipeline order), `chief` is `crew.first`, `robot_count` is `crew.size`, and `network?` is `true`. `run(message = nil, **opts)` accepts a positional message the same way `Robot#run` does — it's folded into `message:` — while the existing `run(message: ...)` keyword form still works unchanged. See [Runnable Protocol](../architecture/core-concepts.md#runnable-protocol) in Core Concepts for the full comparison against `Robot`.
25
+
22
26
  ## Creating Networks
23
27
 
24
28
  Networks are created via `RobotLab.create_network` with a block DSL:
@@ -42,6 +46,10 @@ result = network.run(message: "Analyze this quarterly data")
42
46
  Tasks can have per-task configuration that is deep-merged with network run params:
43
47
 
44
48
  ```ruby
49
+ # The allowlist below matches because the tool was attached as a CLASS.
50
+ billing_robot = RobotLab.build(name: "billing", system_prompt: "...",
51
+ local_tools: [RefundTool])
52
+
45
53
  network = RobotLab.create_network(name: "support") do
46
54
  task :classifier, classifier_robot, depends_on: :none
47
55
  task :billing, billing_robot,
@@ -55,6 +63,28 @@ network = RobotLab.create_network(name: "support") do
55
63
  end
56
64
  ```
57
65
 
66
+ !!! warning "An explicit `tools:` array must match the attachment form"
67
+ `ToolConfig.filter_tools` selects with `allowed_set.include?(tool_name(tool))`,
68
+ and `tool_name` is just `tool.name.to_s`. But `Class#name` and
69
+ `RubyLLM::Tool#name` return different strings, so the entry you list has to
70
+ match how the tool was attached:
71
+
72
+ | Attached as | Allowlist entry | Result |
73
+ |-------------|-----------------|--------|
74
+ | `local_tools: [RefundTool]` (class) | `tools: [RefundTool]` → `"RefundTool"` | matches |
75
+ | `local_tools: [RefundTool]` (class) | `tools: %w[refund]` | no match |
76
+ | `local_tools: [RefundTool.new]` (instance) | `tools: [RefundTool]` | no match |
77
+ | `local_tools: [RefundTool.new]` (instance) | `tools: %w[refund]` | matches |
78
+
79
+ Both forms work; mixing them silently yields an empty tool list. Pick one
80
+ convention per robot and keep the allowlist in the same form.
81
+
82
+ A task's `tools:` is *not* validated — it lands in `run_params` and reaches
83
+ the robot as a runtime value. The robot **constructor**'s `tools:` kwarg is
84
+ validated by `validate_tools_filter!` and raises `ArgumentError` for
85
+ anything that is not a String or Symbol, so the class form shown above is
86
+ usable only at the task/`run` level.
87
+
58
88
  ### Task Parameters
59
89
 
60
90
  | Parameter | Type | Description |
@@ -65,7 +95,11 @@ end
65
95
  | `mcp` | Symbol, Array | MCP server config (`:none`, `:inherit`, or array) |
66
96
  | `tools` | Symbol, Array | Tools config (`:none`, `:inherit`, or array) |
67
97
  | `memory` | Memory, Hash, nil | Task-specific memory |
98
+ | `config` | RunConfig, nil | Per-task RunConfig, merged into the network config the robot sees (`mcp`/`tools` only in practice) |
68
99
  | `depends_on` | Symbol, Array | Dependencies (`:none`, `:optional`, or task names) |
100
+ | `poller_group` | Symbol | Bus poller group label for this robot (default `:default`; purely organizational) |
101
+
102
+ `mcp:` and `tools:` default to `:none` here. Anything other than `:none` is written into `run_params` and reaches the robot as its **runtime** value, so it is resolved against the robot's build-time config exactly as if it had been passed to `run`.
69
103
 
70
104
  ## Execution Model
71
105
 
@@ -94,28 +128,33 @@ stateDiagram-v2
94
128
  Each robot implements the SimpleFlow step interface via `call(result)`:
95
129
 
96
130
  ```ruby
97
- # Inside Robot (simplified)
131
+ # Inside Robot (simplified -- timing and the rescue are elided)
98
132
  def call(result)
99
133
  run_context = extract_run_context(result)
100
134
  message = run_context.delete(:message)
101
135
 
102
136
  robot_result = run(message, **run_context)
137
+ robot_result.duration = ... # monotonic elapsed seconds
103
138
 
104
139
  result
105
- .with_context(@name.to_sym, robot_result)
140
+ .with_context(@name.to_sym, robot_result) # keyed by the ROBOT's name
106
141
  .continue(robot_result)
107
142
  end
108
143
  ```
109
144
 
145
+ The real method also wraps the body so that any exception — including non-`StandardError` ones — is turned into a `RobotResult` whose text is `"Error: <class>: <message>"`, so one failing robot does not crash the pipeline.
146
+
110
147
  ### extract_run_context
111
148
 
112
149
  The `extract_run_context` method pulls parameters from the SimpleFlow result:
113
150
 
114
- - Extracts `:mcp`, `:tools`, `:memory`, and `:network_memory` from `run_params`
115
- - Merges the current result value into the context
116
- - If the previous result value is a `RobotResult`, extracts its `last_text_content` as the message
151
+ - Deletes `:mcp`, `:tools`, `:memory`, `:network_memory`, `:network_config`, `:network`, and `:task` out of `run_params`, then re-attaches them as explicit keyword arguments to `run`
152
+ - `:mcp` and `:tools` default to `:none` when the task did not set them — matching `run`'s own defaults
153
+ - Merges the current result value into the remaining context
154
+ - If the previous result value is a `RobotResult`, uses its `last_text_content` as the message
117
155
  - If it is a String, uses it directly as the message
118
- - If it is a Hash, merges it with the run params
156
+ - If it is a Hash, merges it into the context
157
+ - Anything else is coerced with `to_s` and used as the message
119
158
 
120
159
  ## Task#call Interface
121
160
 
@@ -124,22 +163,36 @@ Each `Task` wraps a robot and enhances the SimpleFlow result before delegation:
124
163
  ```ruby
125
164
  # Inside Task (simplified)
126
165
  def call(result)
127
- # Deep merge task context with run_params
128
- run_params = deep_merge(
129
- result.context[:run_params] || {},
130
- @context
131
- )
132
-
133
- # Add task-specific config
134
- run_params[:mcp] = @mcp unless @mcp == :none
135
- run_params[:tools] = @tools unless @tools == :none
166
+ context = TaskHookContext.new(network: @network, task: self, robot: @robot,
167
+ memory: @memory || @network&.memory, config: @config)
168
+
169
+ RobotLab::Hooks.run(:task, context, registries: [RobotLab.hooks, @network&.hooks]) do
170
+ @robot.call(enhanced_result(result))
171
+ end
172
+ end
173
+
174
+ def enhanced_result(result)
175
+ run_params = deep_merge(result.context[:run_params] || {}, @context)
176
+
177
+ run_params[:mcp] = @mcp unless @mcp == :none
178
+ run_params[:tools] = @tools unless @tools == :none
136
179
  run_params[:memory] = @memory if @memory
137
180
 
138
- enhanced_result = result.with_context(:run_params, run_params)
139
- @robot.call(enhanced_result)
181
+ # Back-references the robot needs for hooks and config resolution
182
+ run_params[:task] = self
183
+ run_params[:network] = @network if @network
184
+
185
+ if @config
186
+ network_rc = run_params[:network_config]
187
+ run_params[:network_config] = network_rc ? network_rc.merge(@config) : @config
188
+ end
189
+
190
+ result.with_context(:run_params, run_params)
140
191
  end
141
192
  ```
142
193
 
194
+ Two things to note. The `:task` hook family is dispatched against `[RobotLab.hooks, network&.hooks]` only — a handler registered with `robot.on` never fires for task hooks. And the task's own `config:` is merged into `run_params[:network_config]`, which is the value `Robot#resolve_mcp_hierarchy` / `#resolve_tools_hierarchy` consult as the parent level.
195
+
143
196
  ## SimpleFlow::Result
144
197
 
145
198
  The result object flows through the pipeline:
@@ -147,30 +200,53 @@ The result object flows through the pipeline:
147
200
  ```ruby
148
201
  result.value # Current task's output (RobotResult)
149
202
  result.context # Accumulated context from all tasks
150
- result.halted? # Whether execution stopped early
151
- result.continued? # Whether execution continues
203
+ result.continue? # Whether execution continues (the only status predicate)
152
204
  ```
153
205
 
154
206
  ### Result Methods
155
207
 
208
+ This is the complete public API of `SimpleFlow::Result` (simple_flow 0.4):
209
+
156
210
  | Method | Description |
157
211
  |--------|-------------|
212
+ | `value` | Current value flowing through the pipeline |
213
+ | `context` | Accumulated context hash |
158
214
  | `continue(value)` | Continue to next tasks |
215
+ | `continue?` | Whether the pipeline is still continuing |
159
216
  | `halt(value)` | Stop pipeline execution |
160
217
  | `with_context(key, val)` | Add data to context |
218
+ | `with_error(key, message)` | Record an error (both arguments required) |
219
+ | `errors` | Recorded errors |
161
220
  | `activate(task_name)` | Enable an optional task |
221
+ | `activated_steps` | Optional tasks that have been activated |
222
+
223
+ There is no `halted?`, no `continued?`, and no `with_value` — use `continue?` for status and `continue(value)` to set a new value.
162
224
 
163
225
  ### Context Structure
164
226
 
165
227
  ```ruby
166
228
  {
167
- run_params: { message: "...", customer_id: 123, network_memory: memory },
168
- classifier: RobotResult, # Stored by Robot#call
229
+ run_params: { message: "...", customer_id: 123,
230
+ network_memory: memory, network: network, task: task },
231
+ classifier: RobotResult, # Stored by Robot#call under the ROBOT's name
169
232
  billing: RobotResult,
170
- # ... other task results
233
+ # ... other robot results
171
234
  }
172
235
  ```
173
236
 
237
+ `Robot#call` stores its output with `result.with_context(@name.to_sym, robot_result)` — the key is the **robot's** `name`, not the task name. The two coincide only when you name them identically:
238
+
239
+ ```ruby
240
+ worker = RobotLab.build(name: "worker_bot", system_prompt: "...")
241
+
242
+ net = RobotLab.create_network(name: "n") do
243
+ task :analysis, worker, depends_on: :none
244
+ end
245
+
246
+ res = net.run(message: "hi")
247
+ res.context.keys #=> [:run_params, :worker_bot] -- not :analysis
248
+ ```
249
+
174
250
  ## Optional Task Activation
175
251
 
176
252
  Optional tasks (those with `depends_on: :optional`) do not run automatically. They must be activated by a preceding task using `result.activate(:task_name)`.
@@ -232,16 +308,37 @@ All robots in a network share the network's memory during execution. The network
232
308
 
233
309
  ```ruby
234
310
  # Inside Network#run
235
- def run(**run_context)
311
+ def run(message = nil, **run_context)
312
+ run_context[:message] = message unless message.nil? # Runnable protocol
313
+
236
314
  run_context[:network_memory] = @memory
237
- initial_result = SimpleFlow::Result.new(
238
- run_context,
239
- context: { run_params: run_context }
240
- )
241
- @pipeline.call_parallel(initial_result)
315
+ run_context[:network] = self
316
+ run_context[:network_config] = @config unless @config.empty?
317
+
318
+ context = NetworkRunHookContext.new(network: self, context: run_context,
319
+ memory: @memory, config: @config)
320
+
321
+ RobotLab::Hooks.run(:network_run, context, registries: [RobotLab.hooks, @hooks]) do
322
+ if @parallel_mode == :ractor
323
+ run_with_ractor_scheduler(context.context)
324
+ else
325
+ initial_result = SimpleFlow::Result.new(
326
+ context.context,
327
+ context: { run_params: context.context }
328
+ )
329
+ @pipeline.call_parallel(initial_result, max_concurrent: @config.max_concurrent_robots)
330
+ end
331
+ end
242
332
  end
243
333
  ```
244
334
 
335
+ Beyond injecting the shared memory, this does four things worth knowing:
336
+
337
+ - It puts the network itself and (when non-empty) the network's `RunConfig` into `run_params`, which is how robots find the parent level for `mcp`/`tools` resolution.
338
+ - The whole run is wrapped in the `:network_run` hook, dispatched against `[RobotLab.hooks, network.hooks]`.
339
+ - `parallel_mode: :ractor` routes to `run_with_ractor_scheduler` instead of the SimpleFlow pipeline; it raises `RobotLab::DependencyError` unless the `robot_lab-ractor` gem is loaded.
340
+ - `max_concurrent:` comes from `@config.max_concurrent_robots` — the one `RunConfig` field the network itself consumes.
341
+
245
342
  Robots use the shared memory for inter-robot communication:
246
343
 
247
344
  ```ruby
@@ -274,7 +371,7 @@ network.broadcast(event: :pause, reason: "rate limit hit")
274
371
  network.broadcast(event: :phase_complete, phase: "analysis")
275
372
  ```
276
373
 
277
- Broadcasts are dispatched asynchronously and also written to memory at the `_network_broadcast` key, so robots can subscribe via `memory.subscribe(:_network_broadcast)`.
374
+ Each handler is invoked inside an `Async { }` block, and the message is also written to memory at the `_network_broadcast` key (`Network::BROADCAST_KEY`), so robots can subscribe via `memory.subscribe(:_network_broadcast)`. Outside a running reactor, `Async { }` runs the block synchronously on the caller's thread. The message handed to a handler is `{ payload:, network:, timestamp: }`, which is why the examples above reach for `message[:payload][:event]`.
278
375
 
279
376
  ## Parallel Execution
280
377
 
@@ -296,17 +393,23 @@ end
296
393
 
297
394
  ### Concurrency Modes
298
395
 
396
+ `create_network`'s `concurrency:` is passed straight to `SimpleFlow::Pipeline`:
397
+
299
398
  | Mode | Description |
300
399
  |------|-------------|
301
- | `:auto` | SimpleFlow chooses best mode |
400
+ | `:auto` | SimpleFlow chooses best mode (default) |
302
401
  | `:threads` | Use Ruby threads |
303
402
  | `:async` | Use async/fiber |
304
403
 
404
+ The number of tasks running at once is capped by the network config's `max_concurrent_robots`, which `Network#run` passes to `call_parallel` as `max_concurrent:`.
405
+
406
+ Separately, `Network.new(parallel_mode:)` selects the execution backend. It defaults to `:async` (the SimpleFlow pipeline above). Setting `parallel_mode: :ractor` bypasses SimpleFlow entirely for a Ractor-based scheduler and requires the `robot_lab-ractor` gem — without it, `run` raises `RobotLab::DependencyError`.
407
+
305
408
  ## Data Flow
306
409
 
307
- 1. **Initial Value**: `network.run(**params)` creates an initial `SimpleFlow::Result` with the run context
308
- 2. **Run Params**: Stored in `result.context[:run_params]`
309
- 3. **Task Results**: Each task adds its `RobotResult` to context under its task name
410
+ 1. **Initial Value**: `network.run(message, **params)` creates an initial `SimpleFlow::Result` with the run context
411
+ 2. **Run Params**: Stored in `result.context[:run_params]`, including `network_memory`, `network`, and (per task) `task`
412
+ 3. **Task Results**: Each robot adds its `RobotResult` to context under **its own `name`**
310
413
  4. **Final Value**: Last task's output becomes `result.value`
311
414
 
312
415
  ```ruby
@@ -315,12 +418,15 @@ result = network.run(
315
418
  customer_id: 123
316
419
  )
317
420
 
318
- result.context[:run_params] #=> { message: "...", customer_id: 123, network_memory: ... }
319
- result.context[:classifier] #=> RobotResult from classifier
320
- result.context[:billing] #=> RobotResult from billing robot
421
+ result.context[:run_params] #=> { message: "...", customer_id: 123,
422
+ # network_memory: ..., network: ..., task: ... }
423
+ result.context[:classifier] #=> RobotResult from the robot NAMED "classifier"
424
+ result.context[:billing] #=> RobotResult from the robot NAMED "billing"
321
425
  result.value #=> Final RobotResult
322
426
  ```
323
427
 
428
+ If a robot's `name:` differs from the task name it was registered under, look it up by the robot's name.
429
+
324
430
  ## Visualization
325
431
 
326
432
  Networks provide visualization methods via the underlying SimpleFlow pipeline:
@@ -342,21 +448,30 @@ puts network.execution_plan
342
448
  ## Network Inspection
343
449
 
344
450
  ```ruby
345
- # Get a robot by name
451
+ # Get a robot by its registration key
346
452
  network.robot(:classifier) #=> Robot
347
453
  network[:classifier] #=> Robot (alias)
348
454
 
349
455
  # List all robots
350
456
  network.available_robots #=> [Robot, Robot, ...]
457
+ network.crew #=> same array, via the Runnable protocol
351
458
 
352
459
  # Add a robot without a task
353
460
  network.add_robot(extra_robot)
354
461
 
462
+ # Remove a dynamically-added robot (returns it, or nil if absent).
463
+ # Only affects the crew (@robots) -- does not rewrite the pipeline.
464
+ network.remove_robot(:extra_robot)
465
+
355
466
  # Convert to hash
356
467
  network.to_h
357
- #=> { name: "support", robots: ["classifier", "billing"], tasks: [...], optional_tasks: [...] }
468
+ #=> { name: "support", robots: ["classifier", "billing"],
469
+ # tasks: ["classifier", "billing"], optional_tasks: [],
470
+ # config: { ... } } # :config present only when the network config is non-empty
358
471
  ```
359
472
 
473
+ The `@robots` hash is keyed **by task name** for robots registered with `task`, but **by `robot.name`** for robots added with `add_robot`. So for a robot built as `name: "worker_bot"` and registered as `task :analysis, worker`, `network.robot(:analysis)` returns it and `network.robot(:worker_bot)` returns `nil` — the opposite of how `result.context` is keyed. `to_h[:robots]` therefore lists task names for task-registered robots.
474
+
360
475
  ## Next Steps
361
476
 
362
477
  - [Memory Management](state-management.md) - Shared memory and reactive features