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
@@ -11,6 +11,17 @@ network = RobotLab.create_network(name: "support", config: config) do
11
11
  end
12
12
  ```
13
13
 
14
+ `RobotLab.create_network(name:, concurrency: :auto, config: nil, &block)` is the
15
+ factory. `Network.new` additionally accepts `memory:` and `parallel_mode:`:
16
+
17
+ ```ruby
18
+ RobotLab::Network.new(name:, concurrency: :auto, memory: nil, config: nil, parallel_mode: :async, &block)
19
+ ```
20
+
21
+ There is no `router:` and no `robots:` keyword. Routing is done by subclassing
22
+ `Robot`, overriding `#call`, and activating optional tasks — see
23
+ [Conditional Routing](#conditional-routing).
24
+
14
25
  ## Attributes
15
26
 
16
27
  ### name
@@ -27,7 +38,37 @@ Network identifier for logging and debugging.
27
38
  network.robots # => Hash<String, Robot>
28
39
  ```
29
40
 
30
- Hash of robots keyed by name.
41
+ Robots keyed by **String**. The key depends on how the robot was registered:
42
+
43
+ | Registered via | Key |
44
+ |----------------|-----|
45
+ | `task(:analyzer, robot)` | the **task** name — `"analyzer"` — regardless of `robot.name` |
46
+ | `add_robot(robot)` | `robot.name` |
47
+
48
+ ```ruby
49
+ n = RobotLab.create_network(name: "n") do
50
+ task :a, RobotLab.build(name: "alpha"), depends_on: :none
51
+ end
52
+ n.add_robot(RobotLab.build(name: "helper"))
53
+
54
+ n.robots.keys # => ["a", "helper"]
55
+ n.crew.map(&:name) # => ["alpha", "helper"]
56
+ ```
57
+
58
+ Examples that assume the two coincide only work when you name the robot after
59
+ its task. `result.context` is keyed by the **robot's** name (`@name.to_sym`), not
60
+ the task name — another reason to keep them identical.
61
+
62
+ ### memory
63
+
64
+ ```ruby
65
+ network.memory # => Memory
66
+ ```
67
+
68
+ Shared reactive memory for every robot in the network. It is passed to each robot
69
+ on `run()` as `network_memory:`, so in-network robots read and write this instance
70
+ instead of their own inherent memory. Created as `Memory.new(network_name: name)`
71
+ unless one is supplied to `Network.new(memory:)`.
31
72
 
32
73
  ### config
33
74
 
@@ -35,7 +76,35 @@ Hash of robots keyed by name.
35
76
  network.config # => RunConfig
36
77
  ```
37
78
 
38
- Shared operational defaults for all robots in the network. Passed to robots during `run()` so they can inherit network-wide LLM settings. See [RunConfig](../../getting-started/configuration.md#runconfig-shared-operational-defaults).
79
+ Shared operational defaults. Passed to robots during `run()` as `network_config:`.
80
+
81
+ !!! warning "Only `mcp` and `tools` propagate to member robots"
82
+ LLM fields (`model`, `temperature`, `max_tokens`, …) and callbacks
83
+ (`on_content`, `on_tool_call`, `on_tool_result`) are read from each robot's
84
+ own config at construction time and are **never** inherited from the
85
+ network. A member robot picks up the network's `mcp`/`tools` only when it
86
+ opts in with `:inherit`. `max_concurrent_robots` is the one field the
87
+ network itself consumes (it is passed to `pipeline.call_parallel`).
88
+
89
+ ### parallel_mode
90
+
91
+ ```ruby
92
+ network.parallel_mode # => :async (default) or :ractor
93
+ ```
94
+
95
+ Execution strategy for `run`. `:async` uses the SimpleFlow pipeline;
96
+ `:ractor` routes through `RactorNetworkScheduler` and **raises
97
+ `RobotLab::DependencyError`** unless the `robot_lab-ractor` gem is loaded. Set via
98
+ `Network.new(parallel_mode:)`.
99
+
100
+ ### hooks
101
+
102
+ ```ruby
103
+ network.hooks # => RobotLab::HookRegistry
104
+ ```
105
+
106
+ The network's own hook registry, populated by [`network.on`](#on). Consulted for
107
+ network-run, robot-run, and task hooks alongside `RobotLab.hooks`.
39
108
 
40
109
  ### pipeline
41
110
 
@@ -56,6 +125,9 @@ result = network.run(
56
125
  **context
57
126
  )
58
127
  # => SimpleFlow::Result
128
+
129
+ # Runnable protocol: a positional message works too, just like Robot#run
130
+ result = network.run("Help me", customer_id: 123)
59
131
  ```
60
132
 
61
133
  Execute the network pipeline.
@@ -64,15 +136,28 @@ Execute the network pipeline.
64
136
 
65
137
  | Name | Type | Description |
66
138
  |------|------|-------------|
67
- | `message` | `String` | The input message |
139
+ | `message` | `String`, `nil` | The input message — as the `message:` keyword, or positionally (folded into `message:` when given) |
68
140
  | `**context` | `Hash` | Additional context passed to all robots |
69
141
 
70
- **Returns:** `SimpleFlow::Result`
142
+ `run` injects three keys into the run context before executing:
143
+ `network_memory:` (the shared `Memory`), `network:` (the network itself), and
144
+ `network_config:` (only when the network's `RunConfig` is non-empty). The whole
145
+ execution is wrapped in the `:network_run` hook family.
146
+
147
+ **Returns:** `SimpleFlow::Result` — except under `parallel_mode: :ractor`, where
148
+ `RactorNetworkScheduler#run_pipeline` returns its own results structure.
149
+
150
+ !!! note "Member robots still default to `tools: :none`"
151
+ Task-level `tools:`/`mcp:` default to `:none` just like `Robot#run`. Declare
152
+ the task with `tools: :inherit` (or an explicit name array) for its robot to
153
+ receive any tools.
71
154
 
72
155
  ### task
73
156
 
74
157
  ```ruby
75
- network.task(name, robot, context: {}, mcp: :none, tools: :none, memory: nil, config: nil, depends_on: :none)
158
+ network.task(name, robot,
159
+ context: {}, mcp: :none, tools: :none, memory: nil,
160
+ config: nil, depends_on: :none, poller_group: :default)
76
161
  # => self
77
162
  ```
78
163
 
@@ -80,16 +165,17 @@ Add a task to the pipeline with optional per-task configuration.
80
165
 
81
166
  **Parameters:**
82
167
 
83
- | Name | Type | Description |
84
- |------|------|-------------|
85
- | `name` | `Symbol` | Task identifier |
86
- | `robot` | `Robot` | Robot instance to execute |
87
- | `context` | `Hash` | Task-specific context (deep-merged with run params) |
88
- | `mcp` | `:none`, Array | MCP server config (`:none` or array of servers) |
89
- | `tools` | `:none`, Array | Tools config (`:none` or array of tools) |
90
- | `memory` | `Memory`, `nil` | Task-specific memory |
91
- | `config` | `RunConfig`, `nil` | Per-task config (merged on top of network's RunConfig) |
92
- | `depends_on` | `:none`, `Array<Symbol>`, `:optional` | Task dependencies |
168
+ | Name | Type | Default | Description |
169
+ |------|------|---------|-------------|
170
+ | `name` | `Symbol` | **required** | Task identifier; also the key under which the robot is stored in `network.robots` |
171
+ | `robot` | `Robot` | **required** | Robot instance to execute |
172
+ | `context` | `Hash` | `{}` | Task-specific context (deep-merged with run params) |
173
+ | `mcp` | `Symbol`, `Array` | `:none` | MCP server config for this task |
174
+ | `tools` | `Symbol`, `Array` | `:none` | Tools config for this task tool **names**, not instances |
175
+ | `memory` | `Memory`, `Hash`, `nil` | `nil` | Task-specific memory, overriding the network's shared memory |
176
+ | `config` | `RunConfig`, `nil` | `nil` | Per-task config, merged on top of the network's RunConfig. Like the network config, only `mcp`/`tools` reach the robot |
177
+ | `depends_on` | `:none`, `Array<Symbol>`, `:optional` | `:none` | Task dependencies |
178
+ | `poller_group` | `Symbol` | `:default` | Bus-poller group for this robot; the network registers the group on its shared `BusPoller` and assigns it to the robot |
93
179
 
94
180
  **Dependency Types:**
95
181
 
@@ -97,7 +183,106 @@ Add a task to the pipeline with optional per-task configuration.
97
183
  |-------|-------------|
98
184
  | `:none` | No dependencies, runs first |
99
185
  | `[:task1, :task2]` | Waits for listed tasks to complete |
100
- | `:optional` | Only runs when explicitly activated |
186
+ | `:optional` | Only runs when explicitly activated via `result.activate(:name)` |
187
+
188
+ ### parallel
189
+
190
+ ```ruby
191
+ network.parallel(name = nil, depends_on: :none) { ... }
192
+ # => self
193
+ ```
194
+
195
+ Declare a named group of steps that run concurrently, then depend on the group as
196
+ a unit. Delegates directly to `SimpleFlow::Pipeline#parallel`.
197
+
198
+ **Parameters:**
199
+
200
+ | Name | Type | Default | Description |
201
+ |------|------|---------|-------------|
202
+ | `name` | `Symbol`, `nil` | `nil` | Optional name for the group, usable in a later `depends_on:` |
203
+ | `depends_on` | `Symbol`, `Array` | `:none` | Dependencies for the group as a whole |
204
+
205
+ !!! warning "Use `step` inside the block, not `task`"
206
+ The block is `instance_eval`'d by `SimpleFlow::Pipeline::ParallelBlock`,
207
+ whose only DSL methods are `step` and `steps` — **not** `task`. This is an
208
+ inconsistency with the top-level `Network` DSL. Inner entries therefore also
209
+ bypass `Network#task`, so they are not registered in `network.robots`, get no
210
+ per-task `context:`/`tools:`/`memory:`, and are not assigned a poller group.
211
+
212
+ ```ruby
213
+ network.parallel :fetch_data, depends_on: :validate do
214
+ step :fetch_orders, orders_robot
215
+ step :fetch_products, products_robot
216
+ end
217
+ network.task :process, processor, depends_on: :fetch_data
218
+ ```
219
+
220
+ Declaring the concurrent robots as ordinary `task`s that share a `depends_on:`
221
+ avoids all of that and is the pattern used throughout these docs.
222
+
223
+ ### broadcast
224
+
225
+ ```ruby
226
+ network.broadcast(payload)
227
+ # => self
228
+ ```
229
+
230
+ Send a network-wide announcement. Wraps `payload` as
231
+ `{ payload:, network:, timestamp: }`, dispatches it asynchronously to every
232
+ handler registered with [`on_broadcast`](#on_broadcast), and also writes it to
233
+ shared memory under `Network::BROADCAST_KEY` (`:_network_broadcast`) so robots can
234
+ pick it up with `memory.subscribe(:_network_broadcast)`.
235
+
236
+ ```ruby
237
+ network.broadcast(event: :pause, reason: "rate limit hit")
238
+ ```
239
+
240
+ ### on_broadcast
241
+
242
+ ```ruby
243
+ network.on_broadcast { |message| ... }
244
+ # => self
245
+ ```
246
+
247
+ Register a handler for `broadcast` messages. The block receives the full envelope
248
+ (`message[:payload]`, `message[:network]`, `message[:timestamp]`).
249
+
250
+ **Raises:** `ArgumentError` if no block is given.
251
+
252
+ ```ruby
253
+ network.on_broadcast do |message|
254
+ pause_current_work if message[:payload][:event] == :pause
255
+ end
256
+ ```
257
+
258
+ ### reset_memory
259
+
260
+ ```ruby
261
+ network.reset_memory
262
+ # => self
263
+ ```
264
+
265
+ Reset the network's shared memory to its initial state (`Memory#reset`). Useful
266
+ between runs. This clears the key-value store only; it does not touch any robot's
267
+ chat history.
268
+
269
+ ### on
270
+
271
+ ```ruby
272
+ network.on(HandlerClass, context: nil)
273
+ ```
274
+
275
+ Register a hook handler on the network's own registry (`network.hooks`). Unlike
276
+ `robot.on`, handlers registered here **do** fire for the `:task` hook family,
277
+ which resolves against `[RobotLab.hooks, network&.hooks]`.
278
+
279
+ ### to_dot
280
+
281
+ ```ruby
282
+ network.to_dot # => String, or nil
283
+ ```
284
+
285
+ Graphviz DOT representation of the pipeline (`pipeline.visualize_dot`).
101
286
 
102
287
  ### add_robot
103
288
 
@@ -108,6 +293,15 @@ network.add_robot(robot)
108
293
 
109
294
  Add a robot without creating a pipeline task. Useful for robots referenced by other tasks.
110
295
 
296
+ ### remove_robot
297
+
298
+ ```ruby
299
+ network.remove_robot("billing")
300
+ # => Robot, or nil if no robot by that name was present
301
+ ```
302
+
303
+ Remove a dynamically-added robot from the crew by name. Complements `add_robot`. Only removes the robot from `@robots` — it does **not** rewrite the pipeline, so don't remove a robot that is a pipeline task (a `depends_on` reference to a removed robot's task name would then fail).
304
+
111
305
  ### robot / []
112
306
 
113
307
  ```ruby
@@ -160,34 +354,58 @@ Hash representation of network configuration.
160
354
  ```ruby
161
355
  {
162
356
  name: "support",
163
- robots: ["classifier", "billing", "technical"],
164
- tasks: ["classifier", "billing", "technical"],
357
+ robots: ["classifier", "billing", "technical"], # keys of network.robots
358
+ tasks: ["classifier", "billing", "technical"], # task names only
165
359
  optional_tasks: [:billing, :technical],
166
- config: { model: "claude-sonnet-4", temperature: 0.7 } # if set
360
+ config: { model: "claude-sonnet-4", temperature: 0.7 }
167
361
  }
168
362
  ```
169
363
 
170
- ## SimpleFlow::Result
364
+ The hash is `.compact`ed and `config` is omitted entirely when the network's
365
+ `RunConfig` is empty. `config` comes from `RunConfig#to_json_hash`, so the
366
+ non-serializable fields (`on_tool_call`, `on_tool_result`, `on_content`, `bus`,
367
+ `auto_compact`) are excluded. `robots` includes robots added with `add_robot`;
368
+ `tasks` does not.
171
369
 
172
- When `run` is called, a `SimpleFlow::Result` is returned:
370
+ ## SimpleFlow::Result
173
371
 
174
- ### Attributes
175
-
176
- ```ruby
177
- result.value # Final task's output (RobotResult)
178
- result.context # Hash of all task results
179
- result.halted? # Whether execution stopped early
180
- result.continued? # Whether execution continues
181
- ```
372
+ `Network#run` returns a `SimpleFlow::Result`. This is its **complete** public API
373
+ (simple_flow 0.4):
374
+
375
+ | Method | Description |
376
+ |--------|-------------|
377
+ | `value` | The final task's output (a `RobotResult`) |
378
+ | `context` | Hash of accumulated context, including every task's result |
379
+ | `continue?` | `true` while the pipeline is still running steps |
380
+ | `continue(value)` | Returns a new Result carrying `value`, still continuing |
381
+ | `halt(value)` | Returns a new Result that stops the pipeline |
382
+ | `with_context(key, value)` | Returns a new Result with an added context entry |
383
+ | `with_error(key, message)` | Returns a new Result with an added error — **both arguments are required** |
384
+ | `errors` | Accumulated errors |
385
+ | `activate(step_name)` | Marks an `:optional` step to run |
386
+ | `activated_steps` | The set of optional steps activated so far |
387
+
388
+ !!! danger "`halted?`, `continued?`, and `with_value` do not exist"
389
+ They raise `NoMethodError`. The predicate is `continue?` — a halted result
390
+ is one where `continue?` is `false`.
391
+
392
+ ```ruby
393
+ result = network.run(message: "Hello")
394
+ puts "stopped early" unless result.continue?
395
+ ```
182
396
 
183
397
  ### Context Structure
184
398
 
185
399
  ```ruby
186
400
  result.context[:run_params] # Original run parameters
187
- result.context[:classifier] # Classifier robot's RobotResult
188
- result.context[:billing] # Billing robot's RobotResult (if activated)
401
+ result.context[:classifier] # RobotResult from the robot NAMED "classifier"
402
+ result.context[:billing] # RobotResult from the robot NAMED "billing"
189
403
  ```
190
404
 
405
+ `Robot#call` writes its result with `result.with_context(@name.to_sym, robot_result)`,
406
+ so context keys are **robot names**, not task names. Name each robot after its
407
+ task to keep the two aligned.
408
+
191
409
  ## Builder DSL
192
410
 
193
411
  ### task
@@ -210,11 +428,17 @@ network = RobotLab.create_network(name: "support") do
210
428
  depends_on: :optional
211
429
  task :technical, technical_robot,
212
430
  context: { department: "technical" },
213
- tools: [DebugTool, LogTool],
431
+ # Entries must match how the robot attached each tool. If technical_robot
432
+ # was built with local_tools: [DebugTool, LogTool] (classes), use the class
433
+ # names; if it attached instances, use %w[debug log] instead.
434
+ tools: %w[DebugTool LogTool],
214
435
  depends_on: :optional
215
436
  end
216
437
  ```
217
438
 
439
+ `tools:` here is a **name allowlist**, exactly as on `Robot`. Attach the tool
440
+ objects themselves with `local_tools:` when building the robot.
441
+
218
442
  ## Examples
219
443
 
220
444
  ### Sequential Pipeline
@@ -247,10 +471,16 @@ end
247
471
 
248
472
  ### Conditional Routing
249
473
 
474
+ There is no `Router` class and no `Router::Args`. Conditional routing is done by
475
+ subclassing `Robot`, overriding `#call`, and calling `result.activate(:task_name)`
476
+ for tasks declared `depends_on: :optional`:
477
+
250
478
  ```ruby
251
479
  class ClassifierRobot < RobotLab::Robot
252
480
  def call(result)
253
- robot_result = run(**extract_run_context(result))
481
+ run_context = extract_run_context(result)
482
+ message = run_context.delete(:message) # must be positional
483
+ robot_result = run(message, **run_context)
254
484
 
255
485
  new_result = result
256
486
  .with_context(@name.to_sym, robot_result)
@@ -294,6 +524,28 @@ elsif result.context[:technical]
294
524
  end
295
525
  ```
296
526
 
527
+ ## Runnable Protocol
528
+
529
+ `Network` includes `RobotLab::Runnable`, the shared interface it has in common with `Robot` — see [Runnable Protocol](../../architecture/network-orchestration.md#runnable-protocol) for the full picture.
530
+
531
+ | Method | Returns |
532
+ |--------|---------|
533
+ | `crew` | `robots.values` — the constituent robots, as an `Array`, in pipeline order |
534
+ | `chief` | `crew.first` — the lead robot |
535
+ | `robot_count` | `crew.size` |
536
+ | `network?` | `true` |
537
+ | `single?` | `false` |
538
+
539
+ ```ruby
540
+ network.crew.map(&:name) # => the ROBOTS' names, not the task keys
541
+ network.chief # => the first robot
542
+ network.network? # => true
543
+ ```
544
+
545
+ `crew` returns `robots.values` — robot *instances*. So `crew.map(&:name)` yields
546
+ `robot.name` for each, which differs from `network.robots.keys` (task names)
547
+ unless each robot is named after its task.
548
+
297
549
  ## See Also
298
550
 
299
551
  - [Creating Networks Guide](../../guides/creating-networks.md)
@@ -8,12 +8,34 @@
8
8
  result = robot.run("What is the capital of France?")
9
9
 
10
10
  result.reply # => "The capital of France is Paris."
11
- result.last_text_content # => alias for reply
12
- result.output # => Array of Message objects (full turn)
13
- result.tool_calls # => Array of ToolResultMessage objects
11
+ result.last_text_content # => reply is an alias for this
12
+ result.output # => [TextMessage] see below
13
+ result.tool_calls # => [] in practice — see below
14
14
  ```
15
15
 
16
- `reply` / `last_text_content` returns the content of the last text message in `output`. This is the string you want for the vast majority of use cases.
16
+ `last_text_content` returns the content of the last text message in `output`;
17
+ `reply` is an alias for it. This is the string you want for the vast majority of
18
+ use cases.
19
+
20
+ !!! note "`output` is not the full turn"
21
+ `output` is built as `[TextMessage.new(role: "assistant", content: text)]`
22
+ from the **final response text only** — a one-element array, or an empty
23
+ array when there is no text. It never contains the user message, the tool
24
+ calls, or the intermediate assistant turns. Read `robot.messages` for the
25
+ real conversation.
26
+
27
+ The text is resolved in this order: `response.content`; then
28
+ `response.thinking.text` (for models that route all output through reasoning
29
+ content, e.g. `qwen3` on Ollama); then the most recent assistant text from
30
+ *the current turn only* in the chat history.
31
+
32
+ !!! note "`tool_calls` is effectively always empty"
33
+ It is populated from the **final** assistant message, which no longer carries
34
+ tool calls once ruby_llm's tool loop has completed. Consequently
35
+ `has_tool_calls?` is also normally `false`. To observe tool activity, use the
36
+ `on_tool_call:` / `on_tool_result:` callbacks or the `:tool_call` hook family.
37
+
38
+ There is **no** `text?` predicate and **no** `content` accessor on `RobotResult`.
17
39
 
18
40
  ## Token & Cost Tracking
19
41
 
@@ -47,77 +69,143 @@ result.duration # => 2.34 (always set by delegate)
47
69
  result.robot_name # => "analyst"
48
70
  result.id # => "550e8400-e29b-..." (UUID, unique per run)
49
71
  result.created_at # => Time instance
50
- result.stop_reason # => "end_turn", "tool_use", or nil
72
+ result.stop_reason # => nil — always, for results built by Robot#run
51
73
  ```
52
74
 
53
- ## Inspecting the Full Output
75
+ !!! warning "`stop_reason` is always `nil` here"
76
+ `build_result` sets it with
77
+ `response.respond_to?(:stop_reason) ? response.stop_reason : nil`, and
78
+ `RubyLLM::Message` does **not** define `stop_reason`. So every
79
+ `robot.run` result carries `nil`, and `export` drops the key entirely.
80
+ (`RobotLab::Message::VALID_STOP_REASONS` is `["tool", "stop"]`, but that
81
+ constant governs the message classes you construct yourself — it is not
82
+ what a `RobotResult` reports.)
83
+
84
+ ## Inspecting the Output
54
85
 
55
86
  ```ruby
56
87
  result.output.each do |message|
57
- puts message.role # :assistant, :tool, etc.
58
- puts message.content # String or Array
88
+ puts message.role # "assistant"
89
+ puts message.content # String
59
90
  end
60
91
 
61
- result.has_tool_calls? # => true if the LLM called any tools
92
+ result.has_tool_calls? # => output.any?(&:tool_call?) || tool_calls.any?
62
93
  result.stopped? # => true if execution ended naturally (not mid-tool-call)
63
94
  ```
64
95
 
96
+ Because `stop_reason` is always `nil` on a `Robot#run` result, `stopped?`
97
+ collapses to `!has_tool_calls?` in practice — and since `tool_calls` is
98
+ effectively always empty, it is normally `true`.
99
+
65
100
  ## Persistence
66
101
 
67
- Export for serialization (excludes debug fields):
102
+ Export for serialization (excludes the debug fields):
68
103
 
69
104
  ```ruby
70
105
  hash = result.export
71
106
  # {
72
107
  # robot_name: "analyst",
108
+ # delegated_by: "manager",
73
109
  # output: [...],
74
110
  # tool_calls: [...],
75
111
  # created_at: "2026-04-18T12:00:00Z",
76
112
  # id: "550e8400-...",
77
113
  # checksum: "a1b2c3...",
78
- # stop_reason: "end_turn",
114
+ # # stop_reason is absent — it is nil, and export is .compact-ed
79
115
  # duration: 2.34,
80
116
  # input_tokens: 512,
81
117
  # output_tokens: 128
82
118
  # }
83
119
 
84
- json = result.to_json
120
+ json = result.to_json # uses export
121
+
122
+ hash_with_debug = result.to_h # export + prompt/history/raw, also compacted
85
123
 
86
124
  # Reconstruct from hash
87
125
  restored = RobotLab::RobotResult.from_hash(hash)
88
126
  ```
89
127
 
90
- `checksum` is a SHA-256 digest of `output + tool_calls + created_at`. Use it for deduplication when persisting results.
128
+ !!! warning "`export` is `.compact`ed"
129
+ Keys whose value is `nil` are dropped entirely. `delegated_by` and
130
+ `duration` disappear when unset, and `stop_reason` is dropped from
131
+ **every** `Robot#run` result because it is always `nil`. `input_tokens` and
132
+ `output_tokens` are additionally converted to `nil` (and therefore dropped)
133
+ when they are **zero**, so a provider that reports no usage yields a hash
134
+ with no token keys at all:
135
+
136
+ ```ruby
137
+ RobotLab::RobotResult.new(robot_name: "a", output: []).export
138
+ # => { robot_name: "a", output: [], tool_calls: [],
139
+ # created_at: "...", id: "...", checksum: "..." }
140
+ ```
141
+
142
+ Only `robot_name`, `output`, `tool_calls`, `created_at`, `id`, and `checksum`
143
+ are always present. Code reading an exported hash must tolerate missing keys.
144
+
145
+ `checksum` is a SHA-256 hex digest of `{ output:, tool_calls:, created_at: }`
146
+ (with `created_at` reduced to an integer epoch). Use it for deduplication when
147
+ persisting results.
91
148
 
92
149
  ## Debug Fields
93
150
 
94
- These are `nil` by default and only populated when explicitly set for debugging:
151
+ All three are read/write and excluded from `export`/`to_json` (but included in `to_h`):
95
152
 
96
153
  ```ruby
97
- result.prompt # Array<Message>prompt sent to the LLM
98
- result.history # Array<Message>history used
99
- result.raw # raw LLM response object from ruby_llm
154
+ result.prompt # Array<Message>, nil nil unless you assign it
155
+ result.history # Array<Message>, nil nil unless you assign it
156
+ result.raw # the raw ruby_llm response; Robot#run always sets this
100
157
  ```
101
158
 
159
+ `raw` is populated by `Robot#build_result` on every run, so it is the escape
160
+ hatch for anything RobotLab does not surface (cost, provider metadata, the
161
+ untouched message object). `prompt` and `history` are never populated by the
162
+ framework.
163
+
102
164
  ## Attribute Reference
103
165
 
104
- | Attribute | Type | Description |
105
- |-----------|------|-------------|
106
- | `robot_name` | String | Name of the robot that produced this result |
107
- | `reply` | String, nil | Last text content (alias: `last_text_content`) |
108
- | `output` | Array\<Message\> | All output messages from this run |
109
- | `tool_calls` | Array\<ToolResultMessage\> | Tool call results |
110
- | `input_tokens` | Integer | Tokens sent to LLM |
111
- | `output_tokens` | Integer | Tokens generated |
112
- | `duration` | Float, nil | Elapsed seconds (set by delegate/pipeline) |
113
- | `delegated_by` | String, nil | Delegating robot's name |
114
- | `id` | String | UUID |
115
- | `created_at` | Time | Creation timestamp |
116
- | `stop_reason` | String, nil | LLM stop reason |
117
- | `checksum` | String | SHA-256 of output content |
166
+ | Attribute | Access | Type | Description |
167
+ |-----------|--------|------|-------------|
168
+ | `robot_name` | r | String | Name of the robot that produced this result |
169
+ | `last_text_content` | r | String, nil | Content of the last text message in `output` (alias: `reply`) |
170
+ | `output` | r | Array\<TextMessage\> | The final response text, as a one-element array (or empty) |
171
+ | `tool_calls` | r | Array\<ToolResultMessage\> | Tool call results — effectively always empty |
172
+ | `input_tokens` | r | Integer | Tokens sent to LLM (0 when the provider reports no usage) |
173
+ | `output_tokens` | r | Integer | Tokens generated (0 when not reported) |
174
+ | `id` | r | String | UUID, generated per result |
175
+ | `created_at` | r | Time | Creation timestamp |
176
+ | `stop_reason` | r | String, nil | Always `nil` for `Robot#run` results (the ruby_llm response has no `stop_reason`); settable only when you build a `RobotResult` yourself |
177
+ | `duration` | rw | Float, nil | Elapsed seconds — set by `delegate` and by `Robot#call` in a pipeline; `nil` for a direct `robot.run` |
178
+ | `delegated_by` | rw | String, nil | Delegating robot's name — set by `delegate` |
179
+ | `prompt` | rw | Array\<Message\>, nil | Debug field, `nil` unless assigned |
180
+ | `history` | rw | Array\<Message\>, nil | Debug field, `nil` unless assigned |
181
+ | `raw` | rw | Object, nil | The raw ruby_llm response; set by `Robot#build_result` |
182
+
183
+ Methods: `checksum`, `export`, `to_h`, `to_json`, `has_tool_calls?`, `stopped?`,
184
+ and the class method `RobotResult.from_hash`.
185
+
186
+ ## Constructor
187
+
188
+ ```ruby
189
+ RobotLab::RobotResult.new(
190
+ robot_name:, # required
191
+ output:, # required — Array<Message, Hash>
192
+ tool_calls: [],
193
+ created_at: nil, # defaults to Time.now
194
+ id: nil, # defaults to SecureRandom.uuid
195
+ prompt: nil,
196
+ history: nil,
197
+ raw: nil,
198
+ stop_reason: nil,
199
+ input_tokens: 0,
200
+ output_tokens: 0
201
+ )
202
+ ```
203
+
204
+ `output` entries must be `Message` instances or Hashes (`Message.from_hash`);
205
+ anything else raises `ArgumentError`.
118
206
 
119
207
  ## Related
120
208
 
121
- - [Robot API](robot.md) — `run`, `delegate`, `compress_history`
209
+ - [Robot API](robot.md) — [`run`](robot.md#run), [`delegate`](robot.md#delegate), [`compress_history`](robot.md#compress_history)
122
210
  - [Building Robots](../../guides/building-robots.md) — Robot construction patterns
123
211
  - [Structured Delegation](../../guides/building-robots.md#structured-delegation) — `DelegationFuture` and async fan-out