robot_lab 0.3.0 → 0.5.0

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 (274) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/README.md +29 -19
  4. data/lib/robot_lab/errors.rb +4 -0
  5. data/lib/robot_lab/hook_context.rb +9 -2
  6. data/lib/robot_lab/mcp/registry.rb +99 -0
  7. data/lib/robot_lab/mcp/server.rb +47 -83
  8. data/lib/robot_lab/mcp/server_discovery.rb +22 -9
  9. data/lib/robot_lab/mcp.rb +259 -0
  10. data/lib/robot_lab/robot/mcp_management.rb +159 -105
  11. data/lib/robot_lab/robot/tool_call_gating.rb +144 -0
  12. data/lib/robot_lab/robot.rb +72 -99
  13. data/lib/robot_lab/run_config.rb +3 -2
  14. data/lib/robot_lab/tool.rb +8 -23
  15. data/lib/robot_lab/version.rb +1 -1
  16. data/lib/robot_lab.rb +1 -4
  17. metadata +16 -292
  18. data/.architecture/AGENTS.md +0 -32
  19. data/.architecture/config.yml +0 -8
  20. data/.architecture/members.yml +0 -60
  21. data/.architecture/reviews/feature-free-will.md +0 -490
  22. data/.architecture/reviews/overall-codebase.md +0 -427
  23. data/.claude/settings.local.json +0 -57
  24. data/.codex/config.toml +0 -2
  25. data/.envrc +0 -6
  26. data/.github/workflows/deploy-github-pages.yml +0 -52
  27. data/.irbrc +0 -6
  28. data/.loki +0 -65
  29. data/.quality/reek_baseline.txt +0 -43
  30. data/.rubocop.yml +0 -10
  31. data/Archspec.rb +0 -44
  32. data/CLAUDE.md +0 -141
  33. data/COMMITS.md +0 -196
  34. data/Rakefile +0 -193
  35. data/_typos.toml +0 -21
  36. data/agent2agent_review.md +0 -192
  37. data/agentf_improvements.md +0 -253
  38. data/agents.md +0 -14
  39. data/docs/api/core/index.md +0 -147
  40. data/docs/api/core/memory.md +0 -578
  41. data/docs/api/core/network.md +0 -553
  42. data/docs/api/core/result.md +0 -211
  43. data/docs/api/core/robot.md +0 -1667
  44. data/docs/api/core/state.md +0 -145
  45. data/docs/api/core/tool.md +0 -520
  46. data/docs/api/errors.md +0 -323
  47. data/docs/api/hooks.md +0 -469
  48. data/docs/api/index.md +0 -137
  49. data/docs/api/mcp/client.md +0 -387
  50. data/docs/api/mcp/index.md +0 -267
  51. data/docs/api/mcp/server.md +0 -252
  52. data/docs/api/mcp/transports.md +0 -339
  53. data/docs/api/messages/index.md +0 -126
  54. data/docs/api/messages/text-message.md +0 -170
  55. data/docs/api/messages/tool-call-message.md +0 -206
  56. data/docs/api/messages/tool-result-message.md +0 -242
  57. data/docs/api/messages/user-message.md +0 -232
  58. data/docs/api/skills.md +0 -423
  59. data/docs/api/streaming/context.md +0 -280
  60. data/docs/api/streaming/events.md +0 -185
  61. data/docs/api/streaming/index.md +0 -156
  62. data/docs/api/support.md +0 -846
  63. data/docs/architecture/core-concepts.md +0 -575
  64. data/docs/architecture/index.md +0 -252
  65. data/docs/architecture/message-flow.md +0 -366
  66. data/docs/architecture/network-orchestration.md +0 -478
  67. data/docs/architecture/robot-execution.md +0 -396
  68. data/docs/architecture/state-management.md +0 -410
  69. data/docs/assets/css/custom.css +0 -56
  70. data/docs/assets/images/robot_lab.jpg +0 -0
  71. data/docs/concepts.md +0 -641
  72. data/docs/examples/basic-chat.md +0 -258
  73. data/docs/examples/index.md +0 -425
  74. data/docs/examples/mcp-server.md +0 -403
  75. data/docs/examples/multi-robot-network.md +0 -476
  76. data/docs/examples/tool-usage.md +0 -468
  77. data/docs/getting-started/configuration.md +0 -645
  78. data/docs/getting-started/index.md +0 -56
  79. data/docs/getting-started/installation.md +0 -197
  80. data/docs/getting-started/quick-start.md +0 -293
  81. data/docs/guides/building-robots.md +0 -1187
  82. data/docs/guides/creating-networks.md +0 -736
  83. data/docs/guides/hooks.md +0 -1206
  84. data/docs/guides/index.md +0 -78
  85. data/docs/guides/knowledge.md +0 -219
  86. data/docs/guides/mcp-integration.md +0 -740
  87. data/docs/guides/memory.md +0 -458
  88. data/docs/guides/observability.md +0 -733
  89. data/docs/guides/streaming.md +0 -323
  90. data/docs/guides/using-tools.md +0 -780
  91. data/docs/index.md +0 -280
  92. data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
  93. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
  94. data/examples/.envrc +0 -3
  95. data/examples/01_simple_robot.rb +0 -31
  96. data/examples/02_tools.rb +0 -109
  97. data/examples/03_network.rb +0 -118
  98. data/examples/04_mcp.rb +0 -217
  99. data/examples/05_streaming.rb +0 -137
  100. data/examples/06_prompt_templates.rb +0 -325
  101. data/examples/07_network_memory.rb +0 -313
  102. data/examples/08_llm_config.rb +0 -162
  103. data/examples/09_chaining.rb +0 -251
  104. data/examples/10_memory.rb +0 -315
  105. data/examples/11_network_introspection.rb +0 -239
  106. data/examples/12_message_bus.rb +0 -67
  107. data/examples/13_spawn.rb +0 -85
  108. data/examples/14_rusty_circuit/.envrc +0 -1
  109. data/examples/14_rusty_circuit/.gitignore +0 -1
  110. data/examples/14_rusty_circuit/README.md +0 -1
  111. data/examples/14_rusty_circuit/comic.rb +0 -149
  112. data/examples/14_rusty_circuit/display.rb +0 -217
  113. data/examples/14_rusty_circuit/heckler.rb +0 -65
  114. data/examples/14_rusty_circuit/open_mic.rb +0 -132
  115. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
  116. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
  117. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
  118. data/examples/14_rusty_circuit/scout.rb +0 -165
  119. data/examples/14_rusty_circuit/show.log +0 -234
  120. data/examples/15_memory_network_and_bus/.envrc +0 -1
  121. data/examples/15_memory_network_and_bus/README.md +0 -66
  122. data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
  123. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
  124. data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
  125. data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
  126. data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
  127. data/examples/15_memory_network_and_bus/output/combined_article.md +0 -11
  128. data/examples/15_memory_network_and_bus/output/final_article.md +0 -8
  129. data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
  130. data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
  131. data/examples/15_memory_network_and_bus/output/memory.json +0 -13
  132. data/examples/15_memory_network_and_bus/output/revision_1.md +0 -29
  133. data/examples/15_memory_network_and_bus/output/revision_2.md +0 -50
  134. data/examples/15_memory_network_and_bus/output/revision_3.md +0 -8
  135. data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
  136. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
  137. data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
  138. data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
  139. data/examples/16_writers_room/.envrc +0 -1
  140. data/examples/16_writers_room/display.rb +0 -158
  141. data/examples/16_writers_room/output/.gitignore +0 -4
  142. data/examples/16_writers_room/output/README.md +0 -69
  143. data/examples/16_writers_room/output/opus_001.md +0 -263
  144. data/examples/16_writers_room/output/opus_001_notes.log +0 -470
  145. data/examples/16_writers_room/output/opus_002.md +0 -245
  146. data/examples/16_writers_room/output/opus_002_notes.log +0 -546
  147. data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
  148. data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
  149. data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
  150. data/examples/16_writers_room/prompts/writer.md +0 -37
  151. data/examples/16_writers_room/room.rb +0 -186
  152. data/examples/16_writers_room/tools.rb +0 -173
  153. data/examples/16_writers_room/writer.rb +0 -121
  154. data/examples/16_writers_room/writers_room.rb +0 -256
  155. data/examples/17_skills.rb +0 -234
  156. data/examples/18_rails/.envrc +0 -3
  157. data/examples/18_rails/.gitignore +0 -5
  158. data/examples/18_rails/Gemfile +0 -11
  159. data/examples/18_rails/README.md +0 -67
  160. data/examples/18_rails/Rakefile +0 -4
  161. data/examples/18_rails/app/controllers/application_controller.rb +0 -4
  162. data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
  163. data/examples/18_rails/app/jobs/application_job.rb +0 -4
  164. data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
  165. data/examples/18_rails/app/models/application_record.rb +0 -5
  166. data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
  167. data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
  168. data/examples/18_rails/app/robots/chat_robot.rb +0 -25
  169. data/examples/18_rails/app/tools/time_tool.rb +0 -9
  170. data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
  171. data/examples/18_rails/app/views/chat/index.html.erb +0 -67
  172. data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
  173. data/examples/18_rails/bin/dev +0 -7
  174. data/examples/18_rails/bin/rails +0 -6
  175. data/examples/18_rails/bin/setup +0 -15
  176. data/examples/18_rails/config/application.rb +0 -33
  177. data/examples/18_rails/config/cable.yml +0 -2
  178. data/examples/18_rails/config/database.yml +0 -5
  179. data/examples/18_rails/config/environment.rb +0 -4
  180. data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
  181. data/examples/18_rails/config/routes.rb +0 -6
  182. data/examples/18_rails/config.ru +0 -4
  183. data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
  184. data/examples/19_token_tracking.rb +0 -138
  185. data/examples/20_circuit_breaker.rb +0 -147
  186. data/examples/21_learning_loop.rb +0 -181
  187. data/examples/22_context_compression.rb +0 -172
  188. data/examples/23_convergence.rb +0 -133
  189. data/examples/24_structured_delegation.rb +0 -151
  190. data/examples/25_history_search/conversation.jsonl +0 -30
  191. data/examples/25_history_search.rb +0 -133
  192. data/examples/26_document_store/api_versioning_adr.md +0 -52
  193. data/examples/26_document_store/incident_postmortem.md +0 -46
  194. data/examples/26_document_store/postgres_runbook.md +0 -49
  195. data/examples/26_document_store/redis_caching_guide.md +0 -48
  196. data/examples/26_document_store/sidekiq_guide.md +0 -51
  197. data/examples/26_document_store.rb +0 -140
  198. data/examples/27_incident_response/README.md +0 -65
  199. data/examples/27_incident_response/incident_response.rb +0 -261
  200. data/examples/28_mcp_discovery.rb +0 -113
  201. data/examples/29_ractor_tools.rb +0 -240
  202. data/examples/30_ractor_network.rb +0 -252
  203. data/examples/31_launch_assessment.rb +0 -246
  204. data/examples/32_newsletter_reader.rb +0 -193
  205. data/examples/33_stock_generator.rb +0 -80
  206. data/examples/33_stock_predictor.rb +0 -327
  207. data/examples/34_agentskills.rb +0 -76
  208. data/examples/35_hooks.rb +0 -266
  209. data/examples/README.md +0 -550
  210. data/examples/common.rb +0 -180
  211. data/examples/prompts/assistant.md +0 -5
  212. data/examples/prompts/audit_trail.md +0 -8
  213. data/examples/prompts/billing.md +0 -10
  214. data/examples/prompts/classifier.md +0 -7
  215. data/examples/prompts/comedian.md +0 -6
  216. data/examples/prompts/comedy_critic.md +0 -10
  217. data/examples/prompts/configurable.md +0 -9
  218. data/examples/prompts/dispatcher.md +0 -12
  219. data/examples/prompts/entity_extractor.md +0 -14
  220. data/examples/prompts/escalation.md +0 -42
  221. data/examples/prompts/frontmatter_mcp_test.md +0 -9
  222. data/examples/prompts/frontmatter_named_test.md +0 -5
  223. data/examples/prompts/frontmatter_tools_test.md +0 -6
  224. data/examples/prompts/general.md +0 -7
  225. data/examples/prompts/github_assistant.md +0 -14
  226. data/examples/prompts/helper.md +0 -4
  227. data/examples/prompts/incident_responder.md +0 -18
  228. data/examples/prompts/keyword_extractor.md +0 -11
  229. data/examples/prompts/llm_config_demo.md +0 -20
  230. data/examples/prompts/order_support.md +0 -35
  231. data/examples/prompts/os_advocate.md +0 -13
  232. data/examples/prompts/os_chief.md +0 -13
  233. data/examples/prompts/os_editor.md +0 -13
  234. data/examples/prompts/parameterized_main_test.md +0 -6
  235. data/examples/prompts/pii_redactor.md +0 -7
  236. data/examples/prompts/product_support.md +0 -37
  237. data/examples/prompts/runbook_protocol.md +0 -12
  238. data/examples/prompts/sentiment_analyzer.md +0 -12
  239. data/examples/prompts/skill_a_test.md +0 -4
  240. data/examples/prompts/skill_b_test.md +0 -4
  241. data/examples/prompts/skill_config_test.md +0 -5
  242. data/examples/prompts/skill_cycle_a_test.md +0 -6
  243. data/examples/prompts/skill_cycle_b_test.md +0 -6
  244. data/examples/prompts/skill_description_test.md +0 -4
  245. data/examples/prompts/skill_leaf_test.md +0 -4
  246. data/examples/prompts/skill_nested_test.md +0 -6
  247. data/examples/prompts/skill_refs_main_test.md +0 -6
  248. data/examples/prompts/skill_self_ref_test.md +0 -6
  249. data/examples/prompts/skill_with_mcp_test.md +0 -9
  250. data/examples/prompts/skill_with_params_test.md +0 -6
  251. data/examples/prompts/skill_with_robot_name_test.md +0 -5
  252. data/examples/prompts/skill_with_tools_test.md +0 -6
  253. data/examples/prompts/sre_compliance.md +0 -10
  254. data/examples/prompts/structured_output.md +0 -7
  255. data/examples/prompts/synthesizer.md +0 -17
  256. data/examples/prompts/technical.md +0 -10
  257. data/examples/prompts/template_with_skills_test.md +0 -6
  258. data/examples/prompts/triage.md +0 -22
  259. data/examples/ruboruby.md +0 -423
  260. data/examples/run_all.rb +0 -60
  261. data/examples/xyzzy.rb +0 -97
  262. data/lib/robot_lab/mcp/client.rb +0 -223
  263. data/lib/robot_lab/mcp/connection_poller.rb +0 -195
  264. data/lib/robot_lab/mcp/transports/base.rb +0 -64
  265. data/lib/robot_lab/mcp/transports/sse.rb +0 -120
  266. data/lib/robot_lab/mcp/transports/stdio.rb +0 -170
  267. data/lib/robot_lab/mcp/transports/streamable_http.rb +0 -141
  268. data/lib/robot_lab/mcp/transports/websocket.rb +0 -111
  269. data/logfile +0 -8
  270. data/mkdocs.yml +0 -209
  271. data/robot_concurrency.md +0 -38
  272. data/simple_acp_review.md +0 -298
  273. data/temp.md +0 -6
  274. data/tool_manifest_plan.md +0 -155
@@ -1,736 +0,0 @@
1
- # Creating Networks
2
-
3
- Networks orchestrate multiple robots using [SimpleFlow](https://github.com/MadBomber/simple_flow) pipelines with DAG-based execution and optional task activation.
4
-
5
- ## Basic Network
6
-
7
- Create a network with a sequential pipeline:
8
-
9
- ```ruby
10
- network = RobotLab.create_network(name: "pipeline") do
11
- task :analyzer, analyzer_robot, depends_on: :none
12
- task :writer, writer_robot, depends_on: [:analyzer]
13
- task :reviewer, reviewer_robot, depends_on: [:writer]
14
- end
15
-
16
- result = network.run(message: "Analyze this document")
17
- ```
18
-
19
- ## Network Properties
20
-
21
- ### Name
22
-
23
- Identifies the network for logging and debugging:
24
-
25
- ```ruby
26
- network = RobotLab.create_network(name: "customer_service") do
27
- # ...
28
- end
29
- ```
30
-
31
- ### Concurrency
32
-
33
- Control parallel execution mode:
34
-
35
- ```ruby
36
- network = RobotLab.create_network(name: "parallel", concurrency: :threads) do
37
- # :auto (default), :threads, or :async
38
- end
39
- ```
40
-
41
- ### Shared Memory
42
-
43
- Networks provide a shared memory accessible to all robots:
44
-
45
- ```ruby
46
- network = RobotLab.create_network(name: "pipeline") do
47
- task :first, robot1, depends_on: :none
48
- end
49
-
50
- # Pre-populate shared memory
51
- network.memory[:project] = "Q4 Report"
52
- network.memory[:user_id] = 123
53
- ```
54
-
55
- ## Adding Tasks
56
-
57
- ### Sequential Tasks
58
-
59
- Each task depends on the previous:
60
-
61
- ```ruby
62
- network = RobotLab.create_network(name: "pipeline") do
63
- task :first, robot1, depends_on: :none
64
- task :second, robot2, depends_on: [:first]
65
- task :third, robot3, depends_on: [:second]
66
- end
67
- ```
68
-
69
- ### Parallel Tasks
70
-
71
- Tasks with the same dependencies run in parallel:
72
-
73
- ```ruby
74
- network = RobotLab.create_network(name: "parallel_analysis") do
75
- task :fetch, fetcher, depends_on: :none
76
-
77
- # These run in parallel after :fetch
78
- task :sentiment, sentiment_bot, depends_on: [:fetch]
79
- task :entities, entity_bot, depends_on: [:fetch]
80
- task :keywords, keyword_bot, depends_on: [:fetch]
81
-
82
- # This waits for all three to complete
83
- task :merge, merger, depends_on: [:sentiment, :entities, :keywords]
84
- end
85
- ```
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
-
103
- ### Concurrency Cap
104
-
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:
106
-
107
- ```ruby
108
- config = RobotLab::RunConfig.new(max_concurrent_robots: 4)
109
-
110
- network = RobotLab.create_network(name: "launch_assessment", config: config) do
111
- # All six declared parallel, but at most 4 LLM calls in-flight simultaneously
112
- task :market, market_robot, depends_on: :none
113
- task :competitive, comp_robot, depends_on: :none
114
- task :tech, tech_robot, depends_on: :none
115
- task :risk, risk_robot, depends_on: :none
116
- task :financial, financial_robot, depends_on: :none # queues until a slot opens
117
- task :legal, legal_robot, depends_on: :none # queues until a slot opens
118
-
119
- task :director, director_robot, depends_on: [:market, :competitive, :tech,
120
- :risk, :financial, :legal]
121
- end
122
- ```
123
-
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.
133
-
134
- ### Optional Tasks
135
-
136
- Optional tasks only run when explicitly activated by a preceding robot:
137
-
138
- ```ruby
139
- network = RobotLab.create_network(name: "router") do
140
- task :classifier, classifier_robot, depends_on: :none
141
- task :billing, billing_robot, depends_on: :optional
142
- task :technical, technical_robot, depends_on: :optional
143
- task :general, general_robot, depends_on: :optional
144
- end
145
- ```
146
-
147
- ## Per-Task Configuration
148
-
149
- Tasks can have individual context and configuration that is deep-merged with the network's run parameters:
150
-
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.
154
- network = RobotLab.create_network(name: "support") do
155
- task :classifier, classifier_robot, depends_on: :none
156
- task :billing, billing_robot,
157
- context: { department: "billing", escalation_level: 2 },
158
- depends_on: :optional
159
- task :technical, technical_robot,
160
- context: { department: "technical" },
161
- tools: [DebugTool, LogTool],
162
- depends_on: :optional
163
- end
164
- ```
165
-
166
- ### Task Options
167
-
168
- | Option | Description |
169
- |--------|-------------|
170
- | `context` | Hash merged with run params (task values override) |
171
- | `mcp` | MCP servers for this task (`:none`, `:inherit`, or array) |
172
- | `tools` | Tools available to this task (`:none`, `:inherit`, or array) |
173
- | `memory` | Task-specific memory |
174
- | `config` | Per-task `RunConfig` (merged on top of network's config) |
175
- | `depends_on` | `:none`, `[:task1]`, or `:optional` |
176
- | `poller_group` | Bus delivery group label (`:default`, `:slow`, etc.) |
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
-
214
- ## Conditional Routing
215
-
216
- Use optional tasks with custom Robot subclasses for intelligent routing:
217
-
218
- ```ruby
219
- class ClassifierRobot < RobotLab::Robot
220
- def call(result)
221
- context = extract_run_context(result)
222
- message = context.delete(:message)
223
- robot_result = run(message, **context)
224
-
225
- new_result = result
226
- .with_context(@name.to_sym, robot_result)
227
- .continue(robot_result)
228
-
229
- # Activate appropriate specialist based on classification
230
- category = robot_result.last_text_content.to_s.strip.downcase
231
-
232
- case category
233
- when /billing/ then new_result.activate(:billing)
234
- when /technical/ then new_result.activate(:technical)
235
- else new_result.activate(:general)
236
- end
237
- end
238
- end
239
-
240
- classifier = ClassifierRobot.new(
241
- name: "classifier",
242
- system_prompt: "Classify as: billing, technical, or general. Respond with one word."
243
- )
244
-
245
- network = RobotLab.create_network(name: "support") do
246
- task :classifier, classifier, depends_on: :none
247
- task :billing, billing_robot, depends_on: :optional
248
- task :technical, technical_robot, depends_on: :optional
249
- task :general, general_robot, depends_on: :optional
250
- end
251
- ```
252
-
253
- ## Poller Groups
254
-
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.
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
-
264
- Named **poller groups** let you label tasks so slow robots are identifiable in logs and monitoring without needing separate infrastructure:
265
-
266
- ```ruby
267
- network = RobotLab.create_network(name: "mixed_speed") do
268
- # Fast robots on the default group
269
- task :fetcher, fetcher_robot, depends_on: :none
270
- task :summarize, summarizer, depends_on: [:fetcher]
271
-
272
- # Slow robots with expensive LLM calls — label them :slow
273
- task :analyst, analyst_robot, depends_on: [:fetcher], poller_group: :slow
274
- task :writer, writer_robot, depends_on: [:analyst], poller_group: :slow
275
- end
276
- ```
277
-
278
- Group labels are informational — there is no separate queue per group. In Async execution, robots naturally yield during LLM HTTP calls, so fast and slow robots interleave without explicit isolation.
279
-
280
- ## Running Networks
281
-
282
- ### Basic Run
283
-
284
- ```ruby
285
- result = network.run(message: "Help me with my order")
286
-
287
- # Get the final response
288
- puts result.value.last_text_content
289
- ```
290
-
291
- ### With Additional Context
292
-
293
- ```ruby
294
- result = network.run(
295
- message: "Check my order status",
296
- customer_id: 123,
297
- order_id: "ORD-456"
298
- )
299
- ```
300
-
301
- ### Accessing Task Results
302
-
303
- ```ruby
304
- result = network.run(message: "Process this")
305
-
306
- # Access individual robot results — keyed by the ROBOT's name
307
- classifier_result = result.context[:classifier]
308
- billing_result = result.context[:billing]
309
-
310
- # Original run parameters
311
- original_params = result.context[:run_params]
312
- ```
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
-
322
- ## SimpleFlow::Result
323
-
324
- Networks return a `SimpleFlow::Result` object:
325
-
326
- ```ruby
327
- result = network.run(message: "Hello")
328
-
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
334
- ```
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
-
347
- ## Broadcasting
348
-
349
- Networks support a broadcast channel for network-wide announcements:
350
-
351
- ```ruby
352
- # Register a broadcast handler
353
- network.on_broadcast do |message|
354
- case message[:payload][:event]
355
- when :pause
356
- puts "Pausing: #{message[:payload][:reason]}"
357
- when :phase_complete
358
- puts "Phase complete: #{message[:payload][:phase]}"
359
- end
360
- end
361
-
362
- # Send broadcasts during execution
363
- network.broadcast(event: :phase_complete, phase: "analysis")
364
- ```
365
-
366
- ## Patterns
367
-
368
- ### Classifier Pattern
369
-
370
- Route to specialists based on classification:
371
-
372
- ```ruby
373
- class SupportClassifier < RobotLab::Robot
374
- def call(result)
375
- context = extract_run_context(result)
376
- message = context.delete(:message)
377
- robot_result = run(message, **context)
378
-
379
- new_result = result
380
- .with_context(@name.to_sym, robot_result)
381
- .continue(robot_result)
382
-
383
- category = robot_result.last_text_content.to_s.strip.downcase
384
- new_result.activate(category.to_sym)
385
- end
386
- end
387
-
388
- network = RobotLab.create_network(name: "support") do
389
- task :classifier, SupportClassifier.new(name: "classifier", template: :classifier),
390
- depends_on: :none
391
- task :billing, billing_robot, depends_on: :optional
392
- task :technical, technical_robot, depends_on: :optional
393
- task :general, general_robot, depends_on: :optional
394
- end
395
- ```
396
-
397
- ### Pipeline Pattern
398
-
399
- Process through sequential stages:
400
-
401
- ```ruby
402
- network = RobotLab.create_network(name: "document_processor") do
403
- task :extract, extractor, depends_on: :none
404
- task :analyze, analyzer, depends_on: [:extract]
405
- task :format, formatter, depends_on: [:analyze]
406
- end
407
- ```
408
-
409
- ### Fan-Out/Fan-In Pattern
410
-
411
- Parallel processing with aggregation:
412
-
413
- ```ruby
414
- network = RobotLab.create_network(name: "multi_analysis") do
415
- task :prepare, preparer, depends_on: :none
416
-
417
- # Fan-out: parallel analysis
418
- task :sentiment, sentiment_analyzer, depends_on: [:prepare]
419
- task :topics, topic_extractor, depends_on: [:prepare]
420
- task :entities, entity_recognizer, depends_on: [:prepare]
421
-
422
- # Fan-in: aggregate results
423
- task :aggregate, aggregator, depends_on: [:sentiment, :topics, :entities]
424
- end
425
- ```
426
-
427
- ### Pipeline Error Resilience
428
-
429
- When a robot raises an exception during pipeline execution, the error is caught and wrapped in a `RobotResult` with the error message as content. This ensures one failing robot does not crash the entire network:
430
-
431
- ```ruby
432
- # If billing_robot raises an error, the network continues
433
- # The error is available in the result context, keyed by the robot's name:
434
- result = network.run(message: "Process this")
435
- billing_result = result.context[:billing]
436
-
437
- if billing_result&.last_text_content&.start_with?("Error:")
438
- puts "Billing failed: #{billing_result.last_text_content}"
439
- puts "Took: #{billing_result.duration}s"
440
- end
441
- ```
442
-
443
- Each robot's `RobotResult` includes a `duration` field (elapsed seconds) that is set automatically during pipeline execution, even for errored results.
444
-
445
- ### Conditional Continuation
446
-
447
- A robot can halt execution early:
448
-
449
- ```ruby
450
- class ValidatorRobot < RobotLab::Robot
451
- def call(result)
452
- context = extract_run_context(result)
453
- message = context.delete(:message)
454
- robot_result = run(message, **context)
455
-
456
- if robot_result.last_text_content.include?("INVALID")
457
- # Stop the pipeline
458
- result.halt(robot_result)
459
- else
460
- # Continue to next task
461
- result
462
- .with_context(@name.to_sym, robot_result)
463
- .continue(robot_result)
464
- end
465
- end
466
- end
467
- ```
468
-
469
- ### Data Passing Between Tasks
470
-
471
- Access previous task results via context:
472
-
473
- ```ruby
474
- class ResponderRobot < RobotLab::Robot
475
- def call(result)
476
- # Get classifier's output — the key is the classifier ROBOT's name
477
- classification = result.context[:classifier]&.last_text_content
478
-
479
- context = extract_run_context(result)
480
- message = context.delete(:message)
481
-
482
- # Use classification in the message or context
483
- robot_result = run(
484
- "Classification: #{classification}\n\nUser message: #{message}",
485
- **context
486
- )
487
-
488
- result.with_context(@name.to_sym, robot_result).continue(robot_result)
489
- end
490
- end
491
- ```
492
-
493
- ## Visualization
494
-
495
- ### ASCII Visualization
496
-
497
- ```ruby
498
- puts network.visualize
499
- # => ASCII representation of the pipeline
500
- ```
501
-
502
- ### Mermaid Diagram
503
-
504
- ```ruby
505
- puts network.to_mermaid
506
- # => Mermaid graph definition
507
- ```
508
-
509
- ### DOT Format (Graphviz)
510
-
511
- ```ruby
512
- puts network.to_dot
513
- # => Graphviz DOT format
514
- ```
515
-
516
- ### Execution Plan
517
-
518
- ```ruby
519
- puts network.execution_plan
520
- # => Description of execution order
521
- ```
522
-
523
- ## Network Introspection
524
-
525
- ```ruby
526
- network.name # => "support"
527
- network.robots # => Hash of key => Robot (see the key caveat below)
528
- network.robot(:billing) # => Robot instance
529
- network["billing"] # => Robot instance (alias)
530
- network.available_robots # => Array of Robot instances
531
- network.crew # => Array of Robot instances (Runnable protocol)
532
- network.memory # => Memory instance (shared)
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
537
- ```
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
-
553
- ## Configuration Inheritance
554
-
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.
568
-
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.
581
-
582
- ```ruby
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
- )
597
-
598
- network = RobotLab.create_network(name: "pipeline", config: shared) do
599
- task :analyzer, analyzer_robot, depends_on: :none, tools: :inherit, mcp: :inherit
600
- task :writer, writer_robot, depends_on: [:analyzer]
601
- end
602
- ```
603
-
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).
640
-
641
- To give every robot the same model, set it on each robot at construction:
642
-
643
- ```ruby
644
- MODEL = "claude-sonnet-4"
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.
657
- network = RobotLab.create_network(name: "pipeline", config: shared) do
658
- task :analyzer, analyzer_robot, depends_on: :none, tools: :inherit
659
- task :writer, writer_robot,
660
- config: RobotLab::RunConfig.new(tools: [DraftTool]), # tools/mcp only
661
- tools: :inherit,
662
- depends_on: [:analyzer]
663
- end
664
- ```
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
-
669
- ### Inheritance Chain
670
-
671
- Per robot, the cascade runs least- to most-specific — note that template front matter is the **base**, not an override:
672
-
673
- ```
674
- template front matter (lowest — the starting point)
675
- -> config: RunConfig
676
- -> constructor kwargs (highest — always wins)
677
- ```
678
-
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.
682
-
683
- ## Best Practices
684
-
685
- ### 1. Keep Robots Focused
686
-
687
- Each robot should have a single responsibility:
688
-
689
- ```ruby
690
- # Good: focused robots
691
- task :classify, classifier, depends_on: :none
692
- task :respond, responder, depends_on: [:classify]
693
-
694
- # Bad: one robot doing everything
695
- task :do_everything, mega_robot, depends_on: :none
696
- ```
697
-
698
- ### 2. Use Per-Task Context
699
-
700
- Pass task-specific configuration through context:
701
-
702
- ```ruby
703
- task :billing, billing_robot,
704
- context: { department: "billing", max_refund: 500 },
705
- depends_on: :optional
706
- ```
707
-
708
- ### 3. Handle Missing Results
709
-
710
- Guard against missing optional task results:
711
-
712
- ```ruby
713
- def call(result)
714
- # Check if the optional task ran (key = that robot's name)
715
- if result.context[:validator]
716
- # Use validator result
717
- else
718
- # Handle missing validation
719
- end
720
- end
721
- ```
722
-
723
- ### 4. Reset Memory Between Runs
724
-
725
- If reusing a network, reset shared memory between runs:
726
-
727
- ```ruby
728
- network.reset_memory
729
- result = network.run(message: "New request")
730
- ```
731
-
732
- ## Next Steps
733
-
734
- - [Using Tools](using-tools.md) - Add capabilities to robots
735
- - [Memory Guide](memory.md) - Shared memory patterns
736
- - [API Reference: Network](../api/core/network.md) - Complete API