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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +1 -1
- data/lib/robot_lab/mcp/transports/streamable_http.rb +1 -1
- data/lib/robot_lab/version.rb +1 -1
- metadata +3 -253
- data/.architecture/AGENTS.md +0 -32
- data/.architecture/config.yml +0 -8
- data/.architecture/members.yml +0 -60
- data/.architecture/reviews/feature-free-will.md +0 -490
- data/.architecture/reviews/overall-codebase.md +0 -427
- data/.claude/settings.local.json +0 -57
- data/.codex/config.toml +0 -2
- data/.envrc +0 -6
- data/.github/workflows/deploy-github-pages.yml +0 -52
- data/.irbrc +0 -6
- data/.loki +0 -65
- data/.quality/reek_baseline.txt +0 -43
- data/.rubocop.yml +0 -10
- data/Archspec.rb +0 -44
- data/CLAUDE.md +0 -141
- data/COMMITS.md +0 -196
- data/Rakefile +0 -193
- data/_typos.toml +0 -21
- data/agent2agent_review.md +0 -192
- data/agentf_improvements.md +0 -253
- data/agents.md +0 -14
- data/docs/api/core/index.md +0 -147
- data/docs/api/core/memory.md +0 -578
- data/docs/api/core/network.md +0 -553
- data/docs/api/core/result.md +0 -211
- data/docs/api/core/robot.md +0 -1667
- data/docs/api/core/state.md +0 -145
- data/docs/api/core/tool.md +0 -520
- data/docs/api/errors.md +0 -323
- data/docs/api/hooks.md +0 -469
- data/docs/api/index.md +0 -137
- data/docs/api/mcp/client.md +0 -387
- data/docs/api/mcp/index.md +0 -267
- data/docs/api/mcp/server.md +0 -252
- data/docs/api/mcp/transports.md +0 -339
- data/docs/api/messages/index.md +0 -126
- data/docs/api/messages/text-message.md +0 -170
- data/docs/api/messages/tool-call-message.md +0 -206
- data/docs/api/messages/tool-result-message.md +0 -242
- data/docs/api/messages/user-message.md +0 -232
- data/docs/api/skills.md +0 -423
- data/docs/api/streaming/context.md +0 -280
- data/docs/api/streaming/events.md +0 -185
- data/docs/api/streaming/index.md +0 -156
- data/docs/api/support.md +0 -846
- data/docs/architecture/core-concepts.md +0 -575
- data/docs/architecture/index.md +0 -252
- data/docs/architecture/message-flow.md +0 -366
- data/docs/architecture/network-orchestration.md +0 -478
- data/docs/architecture/robot-execution.md +0 -396
- data/docs/architecture/state-management.md +0 -410
- data/docs/assets/css/custom.css +0 -56
- data/docs/assets/images/robot_lab.jpg +0 -0
- data/docs/concepts.md +0 -641
- data/docs/examples/basic-chat.md +0 -258
- data/docs/examples/index.md +0 -425
- data/docs/examples/mcp-server.md +0 -403
- data/docs/examples/multi-robot-network.md +0 -476
- data/docs/examples/tool-usage.md +0 -468
- data/docs/getting-started/configuration.md +0 -645
- data/docs/getting-started/index.md +0 -56
- data/docs/getting-started/installation.md +0 -197
- data/docs/getting-started/quick-start.md +0 -293
- data/docs/guides/building-robots.md +0 -1187
- data/docs/guides/creating-networks.md +0 -736
- data/docs/guides/hooks.md +0 -1206
- data/docs/guides/index.md +0 -78
- data/docs/guides/knowledge.md +0 -219
- data/docs/guides/mcp-integration.md +0 -740
- data/docs/guides/memory.md +0 -458
- data/docs/guides/observability.md +0 -733
- data/docs/guides/streaming.md +0 -323
- data/docs/guides/using-tools.md +0 -780
- data/docs/index.md +0 -280
- data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
- data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
- data/examples/.envrc +0 -3
- data/examples/01_simple_robot.rb +0 -31
- data/examples/02_tools.rb +0 -109
- data/examples/03_network.rb +0 -118
- data/examples/04_mcp.rb +0 -217
- data/examples/05_streaming.rb +0 -137
- data/examples/06_prompt_templates.rb +0 -325
- data/examples/07_network_memory.rb +0 -313
- data/examples/08_llm_config.rb +0 -162
- data/examples/09_chaining.rb +0 -251
- data/examples/10_memory.rb +0 -315
- data/examples/11_network_introspection.rb +0 -239
- data/examples/12_message_bus.rb +0 -67
- data/examples/13_spawn.rb +0 -85
- data/examples/14_rusty_circuit/.envrc +0 -1
- data/examples/14_rusty_circuit/.gitignore +0 -1
- data/examples/14_rusty_circuit/README.md +0 -1
- data/examples/14_rusty_circuit/comic.rb +0 -149
- data/examples/14_rusty_circuit/display.rb +0 -217
- data/examples/14_rusty_circuit/heckler.rb +0 -65
- data/examples/14_rusty_circuit/open_mic.rb +0 -132
- data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
- data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
- data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
- data/examples/14_rusty_circuit/scout.rb +0 -165
- data/examples/14_rusty_circuit/show.log +0 -234
- data/examples/15_memory_network_and_bus/.envrc +0 -1
- data/examples/15_memory_network_and_bus/README.md +0 -66
- data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
- data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
- data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
- data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
- data/examples/15_memory_network_and_bus/output/combined_article.md +0 -11
- data/examples/15_memory_network_and_bus/output/final_article.md +0 -8
- data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
- data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
- data/examples/15_memory_network_and_bus/output/memory.json +0 -13
- data/examples/15_memory_network_and_bus/output/revision_1.md +0 -29
- data/examples/15_memory_network_and_bus/output/revision_2.md +0 -50
- data/examples/15_memory_network_and_bus/output/revision_3.md +0 -8
- data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
- data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
- data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
- data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
- data/examples/16_writers_room/.envrc +0 -1
- data/examples/16_writers_room/display.rb +0 -158
- data/examples/16_writers_room/output/.gitignore +0 -4
- data/examples/16_writers_room/output/README.md +0 -69
- data/examples/16_writers_room/output/opus_001.md +0 -263
- data/examples/16_writers_room/output/opus_001_notes.log +0 -470
- data/examples/16_writers_room/output/opus_002.md +0 -245
- data/examples/16_writers_room/output/opus_002_notes.log +0 -546
- data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
- data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
- data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
- data/examples/16_writers_room/prompts/writer.md +0 -37
- data/examples/16_writers_room/room.rb +0 -186
- data/examples/16_writers_room/tools.rb +0 -173
- data/examples/16_writers_room/writer.rb +0 -121
- data/examples/16_writers_room/writers_room.rb +0 -256
- data/examples/17_skills.rb +0 -234
- data/examples/18_rails/.envrc +0 -3
- data/examples/18_rails/.gitignore +0 -5
- data/examples/18_rails/Gemfile +0 -11
- data/examples/18_rails/README.md +0 -67
- data/examples/18_rails/Rakefile +0 -4
- data/examples/18_rails/app/controllers/application_controller.rb +0 -4
- data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
- data/examples/18_rails/app/jobs/application_job.rb +0 -4
- data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
- data/examples/18_rails/app/models/application_record.rb +0 -5
- data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
- data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
- data/examples/18_rails/app/robots/chat_robot.rb +0 -25
- data/examples/18_rails/app/tools/time_tool.rb +0 -9
- data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
- data/examples/18_rails/app/views/chat/index.html.erb +0 -67
- data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
- data/examples/18_rails/bin/dev +0 -7
- data/examples/18_rails/bin/rails +0 -6
- data/examples/18_rails/bin/setup +0 -15
- data/examples/18_rails/config/application.rb +0 -33
- data/examples/18_rails/config/cable.yml +0 -2
- data/examples/18_rails/config/database.yml +0 -5
- data/examples/18_rails/config/environment.rb +0 -4
- data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
- data/examples/18_rails/config/routes.rb +0 -6
- data/examples/18_rails/config.ru +0 -4
- data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
- data/examples/19_token_tracking.rb +0 -138
- data/examples/20_circuit_breaker.rb +0 -147
- data/examples/21_learning_loop.rb +0 -181
- data/examples/22_context_compression.rb +0 -172
- data/examples/23_convergence.rb +0 -133
- data/examples/24_structured_delegation.rb +0 -151
- data/examples/25_history_search/conversation.jsonl +0 -30
- data/examples/25_history_search.rb +0 -133
- data/examples/26_document_store/api_versioning_adr.md +0 -52
- data/examples/26_document_store/incident_postmortem.md +0 -46
- data/examples/26_document_store/postgres_runbook.md +0 -49
- data/examples/26_document_store/redis_caching_guide.md +0 -48
- data/examples/26_document_store/sidekiq_guide.md +0 -51
- data/examples/26_document_store.rb +0 -140
- data/examples/27_incident_response/README.md +0 -65
- data/examples/27_incident_response/incident_response.rb +0 -261
- data/examples/28_mcp_discovery.rb +0 -113
- data/examples/29_ractor_tools.rb +0 -240
- data/examples/30_ractor_network.rb +0 -252
- data/examples/31_launch_assessment.rb +0 -246
- data/examples/32_newsletter_reader.rb +0 -193
- data/examples/33_stock_generator.rb +0 -80
- data/examples/33_stock_predictor.rb +0 -327
- data/examples/34_agentskills.rb +0 -76
- data/examples/35_hooks.rb +0 -266
- data/examples/README.md +0 -550
- data/examples/common.rb +0 -180
- data/examples/prompts/assistant.md +0 -5
- data/examples/prompts/audit_trail.md +0 -8
- data/examples/prompts/billing.md +0 -10
- data/examples/prompts/classifier.md +0 -7
- data/examples/prompts/comedian.md +0 -6
- data/examples/prompts/comedy_critic.md +0 -10
- data/examples/prompts/configurable.md +0 -9
- data/examples/prompts/dispatcher.md +0 -12
- data/examples/prompts/entity_extractor.md +0 -14
- data/examples/prompts/escalation.md +0 -42
- data/examples/prompts/frontmatter_mcp_test.md +0 -9
- data/examples/prompts/frontmatter_named_test.md +0 -5
- data/examples/prompts/frontmatter_tools_test.md +0 -6
- data/examples/prompts/general.md +0 -7
- data/examples/prompts/github_assistant.md +0 -14
- data/examples/prompts/helper.md +0 -4
- data/examples/prompts/incident_responder.md +0 -18
- data/examples/prompts/keyword_extractor.md +0 -11
- data/examples/prompts/llm_config_demo.md +0 -20
- data/examples/prompts/order_support.md +0 -35
- data/examples/prompts/os_advocate.md +0 -13
- data/examples/prompts/os_chief.md +0 -13
- data/examples/prompts/os_editor.md +0 -13
- data/examples/prompts/parameterized_main_test.md +0 -6
- data/examples/prompts/pii_redactor.md +0 -7
- data/examples/prompts/product_support.md +0 -37
- data/examples/prompts/runbook_protocol.md +0 -12
- data/examples/prompts/sentiment_analyzer.md +0 -12
- data/examples/prompts/skill_a_test.md +0 -4
- data/examples/prompts/skill_b_test.md +0 -4
- data/examples/prompts/skill_config_test.md +0 -5
- data/examples/prompts/skill_cycle_a_test.md +0 -6
- data/examples/prompts/skill_cycle_b_test.md +0 -6
- data/examples/prompts/skill_description_test.md +0 -4
- data/examples/prompts/skill_leaf_test.md +0 -4
- data/examples/prompts/skill_nested_test.md +0 -6
- data/examples/prompts/skill_refs_main_test.md +0 -6
- data/examples/prompts/skill_self_ref_test.md +0 -6
- data/examples/prompts/skill_with_mcp_test.md +0 -9
- data/examples/prompts/skill_with_params_test.md +0 -6
- data/examples/prompts/skill_with_robot_name_test.md +0 -5
- data/examples/prompts/skill_with_tools_test.md +0 -6
- data/examples/prompts/sre_compliance.md +0 -10
- data/examples/prompts/structured_output.md +0 -7
- data/examples/prompts/synthesizer.md +0 -17
- data/examples/prompts/technical.md +0 -10
- data/examples/prompts/template_with_skills_test.md +0 -6
- data/examples/prompts/triage.md +0 -22
- data/examples/ruboruby.md +0 -423
- data/examples/run_all.rb +0 -60
- data/examples/xyzzy.rb +0 -97
- data/logfile +0 -8
- data/mkdocs.yml +0 -209
- data/robot_concurrency.md +0 -38
- data/simple_acp_review.md +0 -298
- data/temp.md +0 -6
- 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
|