robot_lab 0.2.8 → 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 (262) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -0
  3. data/README.md +16 -18
  4. data/lib/robot_lab/ask_user.rb +3 -3
  5. data/lib/robot_lab/config/defaults.yml +5 -5
  6. data/lib/robot_lab/config.rb +2 -2
  7. data/lib/robot_lab/mcp/transports/streamable_http.rb +2 -4
  8. data/lib/robot_lab/memory.rb +16 -7
  9. data/lib/robot_lab/robot/hooking.rb +26 -0
  10. data/lib/robot_lab/robot/result_building.rb +119 -0
  11. data/lib/robot_lab/robot.rb +37 -120
  12. data/lib/robot_lab/run_config.rb +52 -20
  13. data/lib/robot_lab/tool.rb +7 -12
  14. data/lib/robot_lab/version.rb +1 -1
  15. data/lib/robot_lab.rb +3 -3
  16. metadata +15 -274
  17. data/.architecture/AGENTS.md +0 -32
  18. data/.architecture/config.yml +0 -8
  19. data/.architecture/members.yml +0 -60
  20. data/.architecture/reviews/feature-free-will.md +0 -490
  21. data/.architecture/reviews/overall-codebase.md +0 -427
  22. data/.claude/settings.local.json +0 -57
  23. data/.codex/config.toml +0 -2
  24. data/.envrc +0 -6
  25. data/.github/workflows/deploy-github-pages.yml +0 -52
  26. data/.irbrc +0 -6
  27. data/.loki +0 -65
  28. data/.quality/reek_baseline.txt +0 -43
  29. data/.rubocop.yml +0 -10
  30. data/Archspec.rb +0 -44
  31. data/CLAUDE.md +0 -139
  32. data/COMMITS.md +0 -196
  33. data/Rakefile +0 -193
  34. data/_typos.toml +0 -21
  35. data/agent2agent_review.md +0 -192
  36. data/agentf_improvements.md +0 -253
  37. data/agents.md +0 -14
  38. data/docs/api/core/index.md +0 -147
  39. data/docs/api/core/memory.md +0 -578
  40. data/docs/api/core/network.md +0 -553
  41. data/docs/api/core/result.md +0 -211
  42. data/docs/api/core/robot.md +0 -1667
  43. data/docs/api/core/state.md +0 -145
  44. data/docs/api/core/tool.md +0 -520
  45. data/docs/api/errors.md +0 -323
  46. data/docs/api/hooks.md +0 -469
  47. data/docs/api/index.md +0 -137
  48. data/docs/api/mcp/client.md +0 -387
  49. data/docs/api/mcp/index.md +0 -267
  50. data/docs/api/mcp/server.md +0 -252
  51. data/docs/api/mcp/transports.md +0 -339
  52. data/docs/api/messages/index.md +0 -126
  53. data/docs/api/messages/text-message.md +0 -170
  54. data/docs/api/messages/tool-call-message.md +0 -206
  55. data/docs/api/messages/tool-result-message.md +0 -242
  56. data/docs/api/messages/user-message.md +0 -232
  57. data/docs/api/skills.md +0 -423
  58. data/docs/api/streaming/context.md +0 -280
  59. data/docs/api/streaming/events.md +0 -185
  60. data/docs/api/streaming/index.md +0 -156
  61. data/docs/api/support.md +0 -846
  62. data/docs/architecture/core-concepts.md +0 -575
  63. data/docs/architecture/index.md +0 -252
  64. data/docs/architecture/message-flow.md +0 -366
  65. data/docs/architecture/network-orchestration.md +0 -478
  66. data/docs/architecture/robot-execution.md +0 -396
  67. data/docs/architecture/state-management.md +0 -410
  68. data/docs/assets/css/custom.css +0 -56
  69. data/docs/assets/images/robot_lab.jpg +0 -0
  70. data/docs/concepts.md +0 -641
  71. data/docs/examples/basic-chat.md +0 -258
  72. data/docs/examples/index.md +0 -425
  73. data/docs/examples/mcp-server.md +0 -403
  74. data/docs/examples/multi-robot-network.md +0 -476
  75. data/docs/examples/tool-usage.md +0 -468
  76. data/docs/getting-started/configuration.md +0 -645
  77. data/docs/getting-started/index.md +0 -56
  78. data/docs/getting-started/installation.md +0 -200
  79. data/docs/getting-started/quick-start.md +0 -293
  80. data/docs/guides/building-robots.md +0 -1187
  81. data/docs/guides/creating-networks.md +0 -736
  82. data/docs/guides/hooks.md +0 -1206
  83. data/docs/guides/index.md +0 -78
  84. data/docs/guides/knowledge.md +0 -219
  85. data/docs/guides/mcp-integration.md +0 -740
  86. data/docs/guides/memory.md +0 -458
  87. data/docs/guides/observability.md +0 -733
  88. data/docs/guides/streaming.md +0 -323
  89. data/docs/guides/using-tools.md +0 -780
  90. data/docs/index.md +0 -280
  91. data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
  92. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
  93. data/examples/.envrc +0 -1
  94. data/examples/01_simple_robot.rb +0 -31
  95. data/examples/02_tools.rb +0 -109
  96. data/examples/03_network.rb +0 -118
  97. data/examples/04_mcp.rb +0 -215
  98. data/examples/05_streaming.rb +0 -137
  99. data/examples/06_prompt_templates.rb +0 -325
  100. data/examples/07_network_memory.rb +0 -313
  101. data/examples/08_llm_config.rb +0 -162
  102. data/examples/09_chaining.rb +0 -251
  103. data/examples/10_memory.rb +0 -315
  104. data/examples/11_network_introspection.rb +0 -239
  105. data/examples/12_message_bus.rb +0 -67
  106. data/examples/13_spawn.rb +0 -85
  107. data/examples/14_rusty_circuit/.envrc +0 -1
  108. data/examples/14_rusty_circuit/.gitignore +0 -1
  109. data/examples/14_rusty_circuit/README.md +0 -1
  110. data/examples/14_rusty_circuit/comic.rb +0 -149
  111. data/examples/14_rusty_circuit/display.rb +0 -217
  112. data/examples/14_rusty_circuit/heckler.rb +0 -65
  113. data/examples/14_rusty_circuit/open_mic.rb +0 -132
  114. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
  115. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
  116. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
  117. data/examples/14_rusty_circuit/scout.rb +0 -165
  118. data/examples/14_rusty_circuit/show.log +0 -234
  119. data/examples/15_memory_network_and_bus/.envrc +0 -1
  120. data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
  121. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
  122. data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
  123. data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
  124. data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
  125. data/examples/15_memory_network_and_bus/output/combined_article.md +0 -13
  126. data/examples/15_memory_network_and_bus/output/final_article.md +0 -13
  127. data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
  128. data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
  129. data/examples/15_memory_network_and_bus/output/memory.json +0 -13
  130. data/examples/15_memory_network_and_bus/output/revision_1.md +0 -18
  131. data/examples/15_memory_network_and_bus/output/revision_2.md +0 -13
  132. data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
  133. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
  134. data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
  135. data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
  136. data/examples/16_writers_room/.envrc +0 -1
  137. data/examples/16_writers_room/display.rb +0 -158
  138. data/examples/16_writers_room/output/.gitignore +0 -4
  139. data/examples/16_writers_room/output/README.md +0 -69
  140. data/examples/16_writers_room/output/opus_001.md +0 -263
  141. data/examples/16_writers_room/output/opus_001_notes.log +0 -470
  142. data/examples/16_writers_room/output/opus_002.md +0 -245
  143. data/examples/16_writers_room/output/opus_002_notes.log +0 -546
  144. data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
  145. data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
  146. data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
  147. data/examples/16_writers_room/prompts/writer.md +0 -37
  148. data/examples/16_writers_room/room.rb +0 -186
  149. data/examples/16_writers_room/tools.rb +0 -173
  150. data/examples/16_writers_room/writer.rb +0 -121
  151. data/examples/16_writers_room/writers_room.rb +0 -256
  152. data/examples/17_skills.rb +0 -234
  153. data/examples/18_rails/.envrc +0 -3
  154. data/examples/18_rails/.gitignore +0 -5
  155. data/examples/18_rails/Gemfile +0 -11
  156. data/examples/18_rails/README.md +0 -67
  157. data/examples/18_rails/Rakefile +0 -4
  158. data/examples/18_rails/app/controllers/application_controller.rb +0 -4
  159. data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
  160. data/examples/18_rails/app/jobs/application_job.rb +0 -4
  161. data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
  162. data/examples/18_rails/app/models/application_record.rb +0 -5
  163. data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
  164. data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
  165. data/examples/18_rails/app/robots/chat_robot.rb +0 -25
  166. data/examples/18_rails/app/tools/time_tool.rb +0 -9
  167. data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
  168. data/examples/18_rails/app/views/chat/index.html.erb +0 -67
  169. data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
  170. data/examples/18_rails/bin/dev +0 -7
  171. data/examples/18_rails/bin/rails +0 -6
  172. data/examples/18_rails/bin/setup +0 -15
  173. data/examples/18_rails/config/application.rb +0 -33
  174. data/examples/18_rails/config/cable.yml +0 -2
  175. data/examples/18_rails/config/database.yml +0 -5
  176. data/examples/18_rails/config/environment.rb +0 -4
  177. data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
  178. data/examples/18_rails/config/routes.rb +0 -6
  179. data/examples/18_rails/config.ru +0 -4
  180. data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
  181. data/examples/19_token_tracking.rb +0 -138
  182. data/examples/20_circuit_breaker.rb +0 -147
  183. data/examples/21_learning_loop.rb +0 -181
  184. data/examples/22_context_compression.rb +0 -172
  185. data/examples/23_convergence.rb +0 -133
  186. data/examples/24_structured_delegation.rb +0 -151
  187. data/examples/25_history_search/conversation.jsonl +0 -30
  188. data/examples/25_history_search.rb +0 -133
  189. data/examples/26_document_store/api_versioning_adr.md +0 -52
  190. data/examples/26_document_store/incident_postmortem.md +0 -46
  191. data/examples/26_document_store/postgres_runbook.md +0 -49
  192. data/examples/26_document_store/redis_caching_guide.md +0 -48
  193. data/examples/26_document_store/sidekiq_guide.md +0 -51
  194. data/examples/26_document_store.rb +0 -140
  195. data/examples/27_incident_response/incident_response.rb +0 -261
  196. data/examples/28_mcp_discovery.rb +0 -113
  197. data/examples/29_ractor_tools.rb +0 -240
  198. data/examples/30_ractor_network.rb +0 -252
  199. data/examples/31_launch_assessment.rb +0 -246
  200. data/examples/32_newsletter_reader.rb +0 -193
  201. data/examples/33_stock_generator.rb +0 -80
  202. data/examples/33_stock_predictor.rb +0 -327
  203. data/examples/34_agentskills.rb +0 -76
  204. data/examples/35_hooks.rb +0 -266
  205. data/examples/README.md +0 -533
  206. data/examples/common.rb +0 -148
  207. data/examples/prompts/assistant.md +0 -5
  208. data/examples/prompts/audit_trail.md +0 -8
  209. data/examples/prompts/billing.md +0 -10
  210. data/examples/prompts/classifier.md +0 -7
  211. data/examples/prompts/comedian.md +0 -6
  212. data/examples/prompts/comedy_critic.md +0 -10
  213. data/examples/prompts/configurable.md +0 -9
  214. data/examples/prompts/dispatcher.md +0 -12
  215. data/examples/prompts/entity_extractor.md +0 -14
  216. data/examples/prompts/escalation.md +0 -42
  217. data/examples/prompts/frontmatter_mcp_test.md +0 -9
  218. data/examples/prompts/frontmatter_named_test.md +0 -5
  219. data/examples/prompts/frontmatter_tools_test.md +0 -6
  220. data/examples/prompts/general.md +0 -7
  221. data/examples/prompts/github_assistant.md +0 -14
  222. data/examples/prompts/helper.md +0 -4
  223. data/examples/prompts/incident_responder.md +0 -18
  224. data/examples/prompts/keyword_extractor.md +0 -11
  225. data/examples/prompts/llm_config_demo.md +0 -20
  226. data/examples/prompts/order_support.md +0 -35
  227. data/examples/prompts/os_advocate.md +0 -13
  228. data/examples/prompts/os_chief.md +0 -13
  229. data/examples/prompts/os_editor.md +0 -13
  230. data/examples/prompts/parameterized_main_test.md +0 -6
  231. data/examples/prompts/pii_redactor.md +0 -7
  232. data/examples/prompts/product_support.md +0 -37
  233. data/examples/prompts/runbook_protocol.md +0 -12
  234. data/examples/prompts/sentiment_analyzer.md +0 -12
  235. data/examples/prompts/skill_a_test.md +0 -4
  236. data/examples/prompts/skill_b_test.md +0 -4
  237. data/examples/prompts/skill_config_test.md +0 -5
  238. data/examples/prompts/skill_cycle_a_test.md +0 -6
  239. data/examples/prompts/skill_cycle_b_test.md +0 -6
  240. data/examples/prompts/skill_description_test.md +0 -4
  241. data/examples/prompts/skill_leaf_test.md +0 -4
  242. data/examples/prompts/skill_nested_test.md +0 -6
  243. data/examples/prompts/skill_refs_main_test.md +0 -6
  244. data/examples/prompts/skill_self_ref_test.md +0 -6
  245. data/examples/prompts/skill_with_mcp_test.md +0 -9
  246. data/examples/prompts/skill_with_params_test.md +0 -6
  247. data/examples/prompts/skill_with_robot_name_test.md +0 -5
  248. data/examples/prompts/skill_with_tools_test.md +0 -6
  249. data/examples/prompts/sre_compliance.md +0 -10
  250. data/examples/prompts/structured_output.md +0 -7
  251. data/examples/prompts/synthesizer.md +0 -17
  252. data/examples/prompts/technical.md +0 -10
  253. data/examples/prompts/template_with_skills_test.md +0 -6
  254. data/examples/prompts/triage.md +0 -22
  255. data/examples/ruboruby.md +0 -423
  256. data/examples/xyzzy.rb +0 -97
  257. data/logfile +0 -8
  258. data/mkdocs.yml +0 -209
  259. data/robot_concurrency.md +0 -38
  260. data/simple_acp_review.md +0 -298
  261. data/temp.md +0 -6
  262. data/tool_manifest_plan.md +0 -155
@@ -1,478 +0,0 @@
1
- # Network Orchestration
2
-
3
- Networks coordinate multiple robots using [SimpleFlow](https://github.com/MadBomber/simple_flow) pipelines for DAG-based execution.
4
-
5
- ## Network Structure
6
-
7
- A network is a thin wrapper around `SimpleFlow::Pipeline`:
8
-
9
- - **Pipeline**: DAG-based execution engine
10
- - **Robots**: Named collection of robot instances
11
- - **Tasks**: Wrap robots with per-task configuration and define dependencies
12
- - **Memory**: Shared reactive memory for inter-robot communication
13
-
14
- ```ruby
15
- network = RobotLab.create_network(name: "customer_service") do
16
- task :classifier, classifier_robot, depends_on: :none
17
- task :billing, billing_robot, depends_on: :optional
18
- task :technical, technical_robot, depends_on: :optional
19
- end
20
- ```
21
-
22
- ## Runnable Protocol
23
-
24
- `Network` implements `RobotLab::Runnable` — the same interface `Robot` implements — so code that needs to run "a robot or a network" doesn't have to branch on `is_a?(RobotLab::Network)`. For a `Network`: `crew` returns `robots.values` (pipeline order), `chief` is `crew.first`, `robot_count` is `crew.size`, and `network?` is `true`. `run(message = nil, **opts)` accepts a positional message the same way `Robot#run` does — it's folded into `message:` — while the existing `run(message: ...)` keyword form still works unchanged. See [Runnable Protocol](../architecture/core-concepts.md#runnable-protocol) in Core Concepts for the full comparison against `Robot`.
25
-
26
- ## Creating Networks
27
-
28
- Networks are created via `RobotLab.create_network` with a block DSL:
29
-
30
- ```ruby
31
- analyst = RobotLab.build(name: "analyst", system_prompt: "Analyze the input.")
32
- writer = RobotLab.build(name: "writer", system_prompt: "Write a report.")
33
- reviewer = RobotLab.build(name: "reviewer", system_prompt: "Review the report.")
34
-
35
- network = RobotLab.create_network(name: "pipeline") do
36
- task :analyst, analyst, depends_on: :none
37
- task :writer, writer, depends_on: [:analyst]
38
- task :reviewer, reviewer, depends_on: [:writer]
39
- end
40
-
41
- result = network.run(message: "Analyze this quarterly data")
42
- ```
43
-
44
- ## Task Configuration
45
-
46
- Tasks can have per-task configuration that is deep-merged with network run params:
47
-
48
- ```ruby
49
- # The allowlist below matches because the tool was attached as a CLASS.
50
- billing_robot = RobotLab.build(name: "billing", system_prompt: "...",
51
- local_tools: [RefundTool])
52
-
53
- network = RobotLab.create_network(name: "support") do
54
- task :classifier, classifier_robot, depends_on: :none
55
- task :billing, billing_robot,
56
- context: { department: "billing", escalation_level: 2 },
57
- tools: [RefundTool],
58
- depends_on: :optional
59
- task :technical, technical_robot,
60
- context: { department: "technical" },
61
- mcp: [filesystem_server],
62
- depends_on: :optional
63
- end
64
- ```
65
-
66
- !!! warning "An explicit `tools:` array must match the attachment form"
67
- `ToolConfig.filter_tools` selects with `allowed_set.include?(tool_name(tool))`,
68
- and `tool_name` is just `tool.name.to_s`. But `Class#name` and
69
- `RubyLLM::Tool#name` return different strings, so the entry you list has to
70
- match how the tool was attached:
71
-
72
- | Attached as | Allowlist entry | Result |
73
- |-------------|-----------------|--------|
74
- | `local_tools: [RefundTool]` (class) | `tools: [RefundTool]` → `"RefundTool"` | matches |
75
- | `local_tools: [RefundTool]` (class) | `tools: %w[refund]` | no match |
76
- | `local_tools: [RefundTool.new]` (instance) | `tools: [RefundTool]` | no match |
77
- | `local_tools: [RefundTool.new]` (instance) | `tools: %w[refund]` | matches |
78
-
79
- Both forms work; mixing them silently yields an empty tool list. Pick one
80
- convention per robot and keep the allowlist in the same form.
81
-
82
- A task's `tools:` is *not* validated — it lands in `run_params` and reaches
83
- the robot as a runtime value. The robot **constructor**'s `tools:` kwarg is
84
- validated by `validate_tools_filter!` and raises `ArgumentError` for
85
- anything that is not a String or Symbol, so the class form shown above is
86
- usable only at the task/`run` level.
87
-
88
- ### Task Parameters
89
-
90
- | Parameter | Type | Description |
91
- |-----------|------|-------------|
92
- | `name` | Symbol | Task/step name |
93
- | `robot` | Robot | The robot instance |
94
- | `context` | Hash | Task-specific context (deep-merged with run params) |
95
- | `mcp` | Symbol, Array | MCP server config (`:none`, `:inherit`, or array) |
96
- | `tools` | Symbol, Array | Tools config (`:none`, `:inherit`, or array) |
97
- | `memory` | Memory, Hash, nil | Task-specific memory |
98
- | `config` | RunConfig, nil | Per-task RunConfig, merged into the network config the robot sees (`mcp`/`tools` only in practice) |
99
- | `depends_on` | Symbol, Array | Dependencies (`:none`, `:optional`, or task names) |
100
- | `poller_group` | Symbol | Bus poller group label for this robot (default `:default`; purely organizational) |
101
-
102
- `mcp:` and `tools:` default to `:none` here. Anything other than `:none` is written into `run_params` and reaches the robot as its **runtime** value, so it is resolved against the robot's build-time config exactly as if it had been passed to `run`.
103
-
104
- ## Execution Model
105
-
106
- ```mermaid
107
- stateDiagram-v2
108
- [*] --> Start
109
- Start --> ExecuteTask: next ready task
110
- ExecuteTask --> CheckDependents: task complete
111
- CheckDependents --> ExecuteTask: more tasks ready
112
- CheckDependents --> Complete: all tasks done
113
- ExecuteTask --> Halted: task halts
114
- Complete --> [*]
115
- Halted --> [*]
116
- ```
117
-
118
- ### Task Dependency Types
119
-
120
- | Type | Description |
121
- |------|-------------|
122
- | `:none` | No dependencies, runs first |
123
- | `[:task1, :task2]` | Waits for listed tasks to complete |
124
- | `:optional` | Only runs when explicitly activated |
125
-
126
- ## Robot#call Interface
127
-
128
- Each robot implements the SimpleFlow step interface via `call(result)`:
129
-
130
- ```ruby
131
- # Inside Robot (simplified -- timing and the rescue are elided)
132
- def call(result)
133
- run_context = extract_run_context(result)
134
- message = run_context.delete(:message)
135
-
136
- robot_result = run(message, **run_context)
137
- robot_result.duration = ... # monotonic elapsed seconds
138
-
139
- result
140
- .with_context(@name.to_sym, robot_result) # keyed by the ROBOT's name
141
- .continue(robot_result)
142
- end
143
- ```
144
-
145
- The real method also wraps the body so that any exception — including non-`StandardError` ones — is turned into a `RobotResult` whose text is `"Error: <class>: <message>"`, so one failing robot does not crash the pipeline.
146
-
147
- ### extract_run_context
148
-
149
- The `extract_run_context` method pulls parameters from the SimpleFlow result:
150
-
151
- - Deletes `:mcp`, `:tools`, `:memory`, `:network_memory`, `:network_config`, `:network`, and `:task` out of `run_params`, then re-attaches them as explicit keyword arguments to `run`
152
- - `:mcp` and `:tools` default to `:none` when the task did not set them — matching `run`'s own defaults
153
- - Merges the current result value into the remaining context
154
- - If the previous result value is a `RobotResult`, uses its `last_text_content` as the message
155
- - If it is a String, uses it directly as the message
156
- - If it is a Hash, merges it into the context
157
- - Anything else is coerced with `to_s` and used as the message
158
-
159
- ## Task#call Interface
160
-
161
- Each `Task` wraps a robot and enhances the SimpleFlow result before delegation:
162
-
163
- ```ruby
164
- # Inside Task (simplified)
165
- def call(result)
166
- context = TaskHookContext.new(network: @network, task: self, robot: @robot,
167
- memory: @memory || @network&.memory, config: @config)
168
-
169
- RobotLab::Hooks.run(:task, context, registries: [RobotLab.hooks, @network&.hooks]) do
170
- @robot.call(enhanced_result(result))
171
- end
172
- end
173
-
174
- def enhanced_result(result)
175
- run_params = deep_merge(result.context[:run_params] || {}, @context)
176
-
177
- run_params[:mcp] = @mcp unless @mcp == :none
178
- run_params[:tools] = @tools unless @tools == :none
179
- run_params[:memory] = @memory if @memory
180
-
181
- # Back-references the robot needs for hooks and config resolution
182
- run_params[:task] = self
183
- run_params[:network] = @network if @network
184
-
185
- if @config
186
- network_rc = run_params[:network_config]
187
- run_params[:network_config] = network_rc ? network_rc.merge(@config) : @config
188
- end
189
-
190
- result.with_context(:run_params, run_params)
191
- end
192
- ```
193
-
194
- Two things to note. The `:task` hook family is dispatched against `[RobotLab.hooks, network&.hooks]` only — a handler registered with `robot.on` never fires for task hooks. And the task's own `config:` is merged into `run_params[:network_config]`, which is the value `Robot#resolve_mcp_hierarchy` / `#resolve_tools_hierarchy` consult as the parent level.
195
-
196
- ## SimpleFlow::Result
197
-
198
- The result object flows through the pipeline:
199
-
200
- ```ruby
201
- result.value # Current task's output (RobotResult)
202
- result.context # Accumulated context from all tasks
203
- result.continue? # Whether execution continues (the only status predicate)
204
- ```
205
-
206
- ### Result Methods
207
-
208
- This is the complete public API of `SimpleFlow::Result` (simple_flow 0.4):
209
-
210
- | Method | Description |
211
- |--------|-------------|
212
- | `value` | Current value flowing through the pipeline |
213
- | `context` | Accumulated context hash |
214
- | `continue(value)` | Continue to next tasks |
215
- | `continue?` | Whether the pipeline is still continuing |
216
- | `halt(value)` | Stop pipeline execution |
217
- | `with_context(key, val)` | Add data to context |
218
- | `with_error(key, message)` | Record an error (both arguments required) |
219
- | `errors` | Recorded errors |
220
- | `activate(task_name)` | Enable an optional task |
221
- | `activated_steps` | Optional tasks that have been activated |
222
-
223
- There is no `halted?`, no `continued?`, and no `with_value` — use `continue?` for status and `continue(value)` to set a new value.
224
-
225
- ### Context Structure
226
-
227
- ```ruby
228
- {
229
- run_params: { message: "...", customer_id: 123,
230
- network_memory: memory, network: network, task: task },
231
- classifier: RobotResult, # Stored by Robot#call under the ROBOT's name
232
- billing: RobotResult,
233
- # ... other robot results
234
- }
235
- ```
236
-
237
- `Robot#call` stores its output with `result.with_context(@name.to_sym, robot_result)` — the key is the **robot's** `name`, not the task name. The two coincide only when you name them identically:
238
-
239
- ```ruby
240
- worker = RobotLab.build(name: "worker_bot", system_prompt: "...")
241
-
242
- net = RobotLab.create_network(name: "n") do
243
- task :analysis, worker, depends_on: :none
244
- end
245
-
246
- res = net.run(message: "hi")
247
- res.context.keys #=> [:run_params, :worker_bot] -- not :analysis
248
- ```
249
-
250
- ## Optional Task Activation
251
-
252
- Optional tasks (those with `depends_on: :optional`) do not run automatically. They must be activated by a preceding task using `result.activate(:task_name)`.
253
-
254
- This pattern is commonly used with a classifier robot that analyzes the input and routes to the appropriate handler:
255
-
256
- ```ruby
257
- classifier = RobotLab.build(
258
- name: "classifier",
259
- system_prompt: "Classify the request. Respond with: BILLING, TECHNICAL, or GENERAL."
260
- )
261
-
262
- billing = RobotLab.build(name: "billing", system_prompt: "Handle billing requests.")
263
- technical = RobotLab.build(name: "technical", system_prompt: "Handle technical requests.")
264
- general = RobotLab.build(name: "general", system_prompt: "Handle general requests.")
265
-
266
- network = RobotLab.create_network(name: "support") do
267
- task :classifier, classifier, depends_on: :none
268
- task :billing, billing, depends_on: :optional
269
- task :technical, technical, depends_on: :optional
270
- task :general, general, depends_on: :optional
271
- end
272
- ```
273
-
274
- ### Classifier Robot Pattern
275
-
276
- To activate optional tasks, a robot subclass overrides `call` to inspect its own output and activate the appropriate downstream task:
277
-
278
- ```ruby
279
- class ClassifierRobot < RobotLab::Robot
280
- def call(result)
281
- run_context = extract_run_context(result)
282
- message = run_context.delete(:message)
283
-
284
- robot_result = run(message, **run_context)
285
-
286
- new_result = result
287
- .with_context(@name.to_sym, robot_result)
288
- .continue(robot_result)
289
-
290
- # Analyze output and activate the appropriate optional task
291
- category = robot_result.last_text_content.to_s.downcase
292
-
293
- case category
294
- when /billing/
295
- new_result.activate(:billing)
296
- when /technical/
297
- new_result.activate(:technical)
298
- else
299
- new_result.activate(:general)
300
- end
301
- end
302
- end
303
- ```
304
-
305
- ## Shared Memory
306
-
307
- All robots in a network share the network's memory during execution. The network injects its memory into the run context:
308
-
309
- ```ruby
310
- # Inside Network#run
311
- def run(message = nil, **run_context)
312
- run_context[:message] = message unless message.nil? # Runnable protocol
313
-
314
- run_context[:network_memory] = @memory
315
- run_context[:network] = self
316
- run_context[:network_config] = @config unless @config.empty?
317
-
318
- context = NetworkRunHookContext.new(network: self, context: run_context,
319
- memory: @memory, config: @config)
320
-
321
- RobotLab::Hooks.run(:network_run, context, registries: [RobotLab.hooks, @hooks]) do
322
- if @parallel_mode == :ractor
323
- run_with_ractor_scheduler(context.context)
324
- else
325
- initial_result = SimpleFlow::Result.new(
326
- context.context,
327
- context: { run_params: context.context }
328
- )
329
- @pipeline.call_parallel(initial_result, max_concurrent: @config.max_concurrent_robots)
330
- end
331
- end
332
- end
333
- ```
334
-
335
- Beyond injecting the shared memory, this does four things worth knowing:
336
-
337
- - It puts the network itself and (when non-empty) the network's `RunConfig` into `run_params`, which is how robots find the parent level for `mcp`/`tools` resolution.
338
- - The whole run is wrapped in the `:network_run` hook, dispatched against `[RobotLab.hooks, network.hooks]`.
339
- - `parallel_mode: :ractor` routes to `run_with_ractor_scheduler` instead of the SimpleFlow pipeline; it raises `RobotLab::DependencyError` unless the `robot_lab-ractor` gem is loaded.
340
- - `max_concurrent:` comes from `@config.max_concurrent_robots` — the one `RunConfig` field the network itself consumes.
341
-
342
- Robots use the shared memory for inter-robot communication:
343
-
344
- ```ruby
345
- # Robot A writes to shared memory
346
- memory.set(:classification, "billing")
347
-
348
- # Robot B reads from shared memory
349
- category = memory.get(:classification, wait: true)
350
- ```
351
-
352
- See [Memory Management](state-management.md) for details on reactive features like subscriptions and blocking reads.
353
-
354
- ## Broadcasting
355
-
356
- Networks support a broadcast channel for network-wide announcements:
357
-
358
- ```ruby
359
- # Register handlers
360
- network.on_broadcast do |message|
361
- case message[:payload][:event]
362
- when :pause
363
- pause_current_work
364
- when :resume
365
- resume_work
366
- end
367
- end
368
-
369
- # Send broadcasts
370
- network.broadcast(event: :pause, reason: "rate limit hit")
371
- network.broadcast(event: :phase_complete, phase: "analysis")
372
- ```
373
-
374
- Each handler is invoked inside an `Async { }` block, and the message is also written to memory at the `_network_broadcast` key (`Network::BROADCAST_KEY`), so robots can subscribe via `memory.subscribe(:_network_broadcast)`. Outside a running reactor, `Async { }` runs the block synchronously on the caller's thread. The message handed to a handler is `{ payload:, network:, timestamp: }`, which is why the examples above reach for `message[:payload][:event]`.
375
-
376
- ## Parallel Execution
377
-
378
- Tasks with the same dependencies can run in parallel:
379
-
380
- ```ruby
381
- network = RobotLab.create_network(name: "analysis", concurrency: :threads) do
382
- task :fetch, fetcher, depends_on: :none
383
-
384
- # These three run in parallel after :fetch completes
385
- task :sentiment, sentiment_bot, depends_on: [:fetch]
386
- task :entities, entity_bot, depends_on: [:fetch]
387
- task :keywords, keyword_bot, depends_on: [:fetch]
388
-
389
- # Waits for all three
390
- task :merge, merger, depends_on: [:sentiment, :entities, :keywords]
391
- end
392
- ```
393
-
394
- ### Concurrency Modes
395
-
396
- `create_network`'s `concurrency:` is passed straight to `SimpleFlow::Pipeline`:
397
-
398
- | Mode | Description |
399
- |------|-------------|
400
- | `:auto` | SimpleFlow chooses best mode (default) |
401
- | `:threads` | Use Ruby threads |
402
- | `:async` | Use async/fiber |
403
-
404
- The number of tasks running at once is capped by the network config's `max_concurrent_robots`, which `Network#run` passes to `call_parallel` as `max_concurrent:`.
405
-
406
- Separately, `Network.new(parallel_mode:)` selects the execution backend. It defaults to `:async` (the SimpleFlow pipeline above). Setting `parallel_mode: :ractor` bypasses SimpleFlow entirely for a Ractor-based scheduler and requires the `robot_lab-ractor` gem — without it, `run` raises `RobotLab::DependencyError`.
407
-
408
- ## Data Flow
409
-
410
- 1. **Initial Value**: `network.run(message, **params)` creates an initial `SimpleFlow::Result` with the run context
411
- 2. **Run Params**: Stored in `result.context[:run_params]`, including `network_memory`, `network`, and (per task) `task`
412
- 3. **Task Results**: Each robot adds its `RobotResult` to context under **its own `name`**
413
- 4. **Final Value**: Last task's output becomes `result.value`
414
-
415
- ```ruby
416
- result = network.run(
417
- message: "Help with billing",
418
- customer_id: 123
419
- )
420
-
421
- result.context[:run_params] #=> { message: "...", customer_id: 123,
422
- # network_memory: ..., network: ..., task: ... }
423
- result.context[:classifier] #=> RobotResult from the robot NAMED "classifier"
424
- result.context[:billing] #=> RobotResult from the robot NAMED "billing"
425
- result.value #=> Final RobotResult
426
- ```
427
-
428
- If a robot's `name:` differs from the task name it was registered under, look it up by the robot's name.
429
-
430
- ## Visualization
431
-
432
- Networks provide visualization methods via the underlying SimpleFlow pipeline:
433
-
434
- ```ruby
435
- # ASCII representation
436
- puts network.visualize
437
-
438
- # Mermaid diagram
439
- puts network.to_mermaid
440
-
441
- # DOT format (Graphviz)
442
- puts network.to_dot
443
-
444
- # Execution plan description
445
- puts network.execution_plan
446
- ```
447
-
448
- ## Network Inspection
449
-
450
- ```ruby
451
- # Get a robot by its registration key
452
- network.robot(:classifier) #=> Robot
453
- network[:classifier] #=> Robot (alias)
454
-
455
- # List all robots
456
- network.available_robots #=> [Robot, Robot, ...]
457
- network.crew #=> same array, via the Runnable protocol
458
-
459
- # Add a robot without a task
460
- network.add_robot(extra_robot)
461
-
462
- # Remove a dynamically-added robot (returns it, or nil if absent).
463
- # Only affects the crew (@robots) -- does not rewrite the pipeline.
464
- network.remove_robot(:extra_robot)
465
-
466
- # Convert to hash
467
- network.to_h
468
- #=> { name: "support", robots: ["classifier", "billing"],
469
- # tasks: ["classifier", "billing"], optional_tasks: [],
470
- # config: { ... } } # :config present only when the network config is non-empty
471
- ```
472
-
473
- The `@robots` hash is keyed **by task name** for robots registered with `task`, but **by `robot.name`** for robots added with `add_robot`. So for a robot built as `name: "worker_bot"` and registered as `task :analysis, worker`, `network.robot(:analysis)` returns it and `network.robot(:worker_bot)` returns `nil` — the opposite of how `result.context` is keyed. `to_h[:robots]` therefore lists task names for task-registered robots.
474
-
475
- ## Next Steps
476
-
477
- - [Memory Management](state-management.md) - Shared memory and reactive features
478
- - [Message Flow](message-flow.md) - How messages are processed within robots