robot_lab 0.3.0 → 0.3.1

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 (256) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +1 -1
  4. data/lib/robot_lab/mcp/transports/streamable_http.rb +1 -1
  5. data/lib/robot_lab/version.rb +1 -1
  6. metadata +3 -253
  7. data/.architecture/AGENTS.md +0 -32
  8. data/.architecture/config.yml +0 -8
  9. data/.architecture/members.yml +0 -60
  10. data/.architecture/reviews/feature-free-will.md +0 -490
  11. data/.architecture/reviews/overall-codebase.md +0 -427
  12. data/.claude/settings.local.json +0 -57
  13. data/.codex/config.toml +0 -2
  14. data/.envrc +0 -6
  15. data/.github/workflows/deploy-github-pages.yml +0 -52
  16. data/.irbrc +0 -6
  17. data/.loki +0 -65
  18. data/.quality/reek_baseline.txt +0 -43
  19. data/.rubocop.yml +0 -10
  20. data/Archspec.rb +0 -44
  21. data/CLAUDE.md +0 -141
  22. data/COMMITS.md +0 -196
  23. data/Rakefile +0 -193
  24. data/_typos.toml +0 -21
  25. data/agent2agent_review.md +0 -192
  26. data/agentf_improvements.md +0 -253
  27. data/agents.md +0 -14
  28. data/docs/api/core/index.md +0 -147
  29. data/docs/api/core/memory.md +0 -578
  30. data/docs/api/core/network.md +0 -553
  31. data/docs/api/core/result.md +0 -211
  32. data/docs/api/core/robot.md +0 -1667
  33. data/docs/api/core/state.md +0 -145
  34. data/docs/api/core/tool.md +0 -520
  35. data/docs/api/errors.md +0 -323
  36. data/docs/api/hooks.md +0 -469
  37. data/docs/api/index.md +0 -137
  38. data/docs/api/mcp/client.md +0 -387
  39. data/docs/api/mcp/index.md +0 -267
  40. data/docs/api/mcp/server.md +0 -252
  41. data/docs/api/mcp/transports.md +0 -339
  42. data/docs/api/messages/index.md +0 -126
  43. data/docs/api/messages/text-message.md +0 -170
  44. data/docs/api/messages/tool-call-message.md +0 -206
  45. data/docs/api/messages/tool-result-message.md +0 -242
  46. data/docs/api/messages/user-message.md +0 -232
  47. data/docs/api/skills.md +0 -423
  48. data/docs/api/streaming/context.md +0 -280
  49. data/docs/api/streaming/events.md +0 -185
  50. data/docs/api/streaming/index.md +0 -156
  51. data/docs/api/support.md +0 -846
  52. data/docs/architecture/core-concepts.md +0 -575
  53. data/docs/architecture/index.md +0 -252
  54. data/docs/architecture/message-flow.md +0 -366
  55. data/docs/architecture/network-orchestration.md +0 -478
  56. data/docs/architecture/robot-execution.md +0 -396
  57. data/docs/architecture/state-management.md +0 -410
  58. data/docs/assets/css/custom.css +0 -56
  59. data/docs/assets/images/robot_lab.jpg +0 -0
  60. data/docs/concepts.md +0 -641
  61. data/docs/examples/basic-chat.md +0 -258
  62. data/docs/examples/index.md +0 -425
  63. data/docs/examples/mcp-server.md +0 -403
  64. data/docs/examples/multi-robot-network.md +0 -476
  65. data/docs/examples/tool-usage.md +0 -468
  66. data/docs/getting-started/configuration.md +0 -645
  67. data/docs/getting-started/index.md +0 -56
  68. data/docs/getting-started/installation.md +0 -197
  69. data/docs/getting-started/quick-start.md +0 -293
  70. data/docs/guides/building-robots.md +0 -1187
  71. data/docs/guides/creating-networks.md +0 -736
  72. data/docs/guides/hooks.md +0 -1206
  73. data/docs/guides/index.md +0 -78
  74. data/docs/guides/knowledge.md +0 -219
  75. data/docs/guides/mcp-integration.md +0 -740
  76. data/docs/guides/memory.md +0 -458
  77. data/docs/guides/observability.md +0 -733
  78. data/docs/guides/streaming.md +0 -323
  79. data/docs/guides/using-tools.md +0 -780
  80. data/docs/index.md +0 -280
  81. data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
  82. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
  83. data/examples/.envrc +0 -3
  84. data/examples/01_simple_robot.rb +0 -31
  85. data/examples/02_tools.rb +0 -109
  86. data/examples/03_network.rb +0 -118
  87. data/examples/04_mcp.rb +0 -217
  88. data/examples/05_streaming.rb +0 -137
  89. data/examples/06_prompt_templates.rb +0 -325
  90. data/examples/07_network_memory.rb +0 -313
  91. data/examples/08_llm_config.rb +0 -162
  92. data/examples/09_chaining.rb +0 -251
  93. data/examples/10_memory.rb +0 -315
  94. data/examples/11_network_introspection.rb +0 -239
  95. data/examples/12_message_bus.rb +0 -67
  96. data/examples/13_spawn.rb +0 -85
  97. data/examples/14_rusty_circuit/.envrc +0 -1
  98. data/examples/14_rusty_circuit/.gitignore +0 -1
  99. data/examples/14_rusty_circuit/README.md +0 -1
  100. data/examples/14_rusty_circuit/comic.rb +0 -149
  101. data/examples/14_rusty_circuit/display.rb +0 -217
  102. data/examples/14_rusty_circuit/heckler.rb +0 -65
  103. data/examples/14_rusty_circuit/open_mic.rb +0 -132
  104. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
  105. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
  106. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
  107. data/examples/14_rusty_circuit/scout.rb +0 -165
  108. data/examples/14_rusty_circuit/show.log +0 -234
  109. data/examples/15_memory_network_and_bus/.envrc +0 -1
  110. data/examples/15_memory_network_and_bus/README.md +0 -66
  111. data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
  112. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
  113. data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
  114. data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
  115. data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
  116. data/examples/15_memory_network_and_bus/output/combined_article.md +0 -11
  117. data/examples/15_memory_network_and_bus/output/final_article.md +0 -8
  118. data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
  119. data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
  120. data/examples/15_memory_network_and_bus/output/memory.json +0 -13
  121. data/examples/15_memory_network_and_bus/output/revision_1.md +0 -29
  122. data/examples/15_memory_network_and_bus/output/revision_2.md +0 -50
  123. data/examples/15_memory_network_and_bus/output/revision_3.md +0 -8
  124. data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
  125. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
  126. data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
  127. data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
  128. data/examples/16_writers_room/.envrc +0 -1
  129. data/examples/16_writers_room/display.rb +0 -158
  130. data/examples/16_writers_room/output/.gitignore +0 -4
  131. data/examples/16_writers_room/output/README.md +0 -69
  132. data/examples/16_writers_room/output/opus_001.md +0 -263
  133. data/examples/16_writers_room/output/opus_001_notes.log +0 -470
  134. data/examples/16_writers_room/output/opus_002.md +0 -245
  135. data/examples/16_writers_room/output/opus_002_notes.log +0 -546
  136. data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
  137. data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
  138. data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
  139. data/examples/16_writers_room/prompts/writer.md +0 -37
  140. data/examples/16_writers_room/room.rb +0 -186
  141. data/examples/16_writers_room/tools.rb +0 -173
  142. data/examples/16_writers_room/writer.rb +0 -121
  143. data/examples/16_writers_room/writers_room.rb +0 -256
  144. data/examples/17_skills.rb +0 -234
  145. data/examples/18_rails/.envrc +0 -3
  146. data/examples/18_rails/.gitignore +0 -5
  147. data/examples/18_rails/Gemfile +0 -11
  148. data/examples/18_rails/README.md +0 -67
  149. data/examples/18_rails/Rakefile +0 -4
  150. data/examples/18_rails/app/controllers/application_controller.rb +0 -4
  151. data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
  152. data/examples/18_rails/app/jobs/application_job.rb +0 -4
  153. data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
  154. data/examples/18_rails/app/models/application_record.rb +0 -5
  155. data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
  156. data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
  157. data/examples/18_rails/app/robots/chat_robot.rb +0 -25
  158. data/examples/18_rails/app/tools/time_tool.rb +0 -9
  159. data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
  160. data/examples/18_rails/app/views/chat/index.html.erb +0 -67
  161. data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
  162. data/examples/18_rails/bin/dev +0 -7
  163. data/examples/18_rails/bin/rails +0 -6
  164. data/examples/18_rails/bin/setup +0 -15
  165. data/examples/18_rails/config/application.rb +0 -33
  166. data/examples/18_rails/config/cable.yml +0 -2
  167. data/examples/18_rails/config/database.yml +0 -5
  168. data/examples/18_rails/config/environment.rb +0 -4
  169. data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
  170. data/examples/18_rails/config/routes.rb +0 -6
  171. data/examples/18_rails/config.ru +0 -4
  172. data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
  173. data/examples/19_token_tracking.rb +0 -138
  174. data/examples/20_circuit_breaker.rb +0 -147
  175. data/examples/21_learning_loop.rb +0 -181
  176. data/examples/22_context_compression.rb +0 -172
  177. data/examples/23_convergence.rb +0 -133
  178. data/examples/24_structured_delegation.rb +0 -151
  179. data/examples/25_history_search/conversation.jsonl +0 -30
  180. data/examples/25_history_search.rb +0 -133
  181. data/examples/26_document_store/api_versioning_adr.md +0 -52
  182. data/examples/26_document_store/incident_postmortem.md +0 -46
  183. data/examples/26_document_store/postgres_runbook.md +0 -49
  184. data/examples/26_document_store/redis_caching_guide.md +0 -48
  185. data/examples/26_document_store/sidekiq_guide.md +0 -51
  186. data/examples/26_document_store.rb +0 -140
  187. data/examples/27_incident_response/README.md +0 -65
  188. data/examples/27_incident_response/incident_response.rb +0 -261
  189. data/examples/28_mcp_discovery.rb +0 -113
  190. data/examples/29_ractor_tools.rb +0 -240
  191. data/examples/30_ractor_network.rb +0 -252
  192. data/examples/31_launch_assessment.rb +0 -246
  193. data/examples/32_newsletter_reader.rb +0 -193
  194. data/examples/33_stock_generator.rb +0 -80
  195. data/examples/33_stock_predictor.rb +0 -327
  196. data/examples/34_agentskills.rb +0 -76
  197. data/examples/35_hooks.rb +0 -266
  198. data/examples/README.md +0 -550
  199. data/examples/common.rb +0 -180
  200. data/examples/prompts/assistant.md +0 -5
  201. data/examples/prompts/audit_trail.md +0 -8
  202. data/examples/prompts/billing.md +0 -10
  203. data/examples/prompts/classifier.md +0 -7
  204. data/examples/prompts/comedian.md +0 -6
  205. data/examples/prompts/comedy_critic.md +0 -10
  206. data/examples/prompts/configurable.md +0 -9
  207. data/examples/prompts/dispatcher.md +0 -12
  208. data/examples/prompts/entity_extractor.md +0 -14
  209. data/examples/prompts/escalation.md +0 -42
  210. data/examples/prompts/frontmatter_mcp_test.md +0 -9
  211. data/examples/prompts/frontmatter_named_test.md +0 -5
  212. data/examples/prompts/frontmatter_tools_test.md +0 -6
  213. data/examples/prompts/general.md +0 -7
  214. data/examples/prompts/github_assistant.md +0 -14
  215. data/examples/prompts/helper.md +0 -4
  216. data/examples/prompts/incident_responder.md +0 -18
  217. data/examples/prompts/keyword_extractor.md +0 -11
  218. data/examples/prompts/llm_config_demo.md +0 -20
  219. data/examples/prompts/order_support.md +0 -35
  220. data/examples/prompts/os_advocate.md +0 -13
  221. data/examples/prompts/os_chief.md +0 -13
  222. data/examples/prompts/os_editor.md +0 -13
  223. data/examples/prompts/parameterized_main_test.md +0 -6
  224. data/examples/prompts/pii_redactor.md +0 -7
  225. data/examples/prompts/product_support.md +0 -37
  226. data/examples/prompts/runbook_protocol.md +0 -12
  227. data/examples/prompts/sentiment_analyzer.md +0 -12
  228. data/examples/prompts/skill_a_test.md +0 -4
  229. data/examples/prompts/skill_b_test.md +0 -4
  230. data/examples/prompts/skill_config_test.md +0 -5
  231. data/examples/prompts/skill_cycle_a_test.md +0 -6
  232. data/examples/prompts/skill_cycle_b_test.md +0 -6
  233. data/examples/prompts/skill_description_test.md +0 -4
  234. data/examples/prompts/skill_leaf_test.md +0 -4
  235. data/examples/prompts/skill_nested_test.md +0 -6
  236. data/examples/prompts/skill_refs_main_test.md +0 -6
  237. data/examples/prompts/skill_self_ref_test.md +0 -6
  238. data/examples/prompts/skill_with_mcp_test.md +0 -9
  239. data/examples/prompts/skill_with_params_test.md +0 -6
  240. data/examples/prompts/skill_with_robot_name_test.md +0 -5
  241. data/examples/prompts/skill_with_tools_test.md +0 -6
  242. data/examples/prompts/sre_compliance.md +0 -10
  243. data/examples/prompts/structured_output.md +0 -7
  244. data/examples/prompts/synthesizer.md +0 -17
  245. data/examples/prompts/technical.md +0 -10
  246. data/examples/prompts/template_with_skills_test.md +0 -6
  247. data/examples/prompts/triage.md +0 -22
  248. data/examples/ruboruby.md +0 -423
  249. data/examples/run_all.rb +0 -60
  250. data/examples/xyzzy.rb +0 -97
  251. data/logfile +0 -8
  252. data/mkdocs.yml +0 -209
  253. data/robot_concurrency.md +0 -38
  254. data/simple_acp_review.md +0 -298
  255. data/temp.md +0 -6
  256. 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