robot_lab 0.2.6 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +124 -64
- data/docs/api/core/index.md +41 -15
- data/docs/api/core/memory.md +247 -29
- data/docs/api/core/network.md +255 -33
- data/docs/api/core/result.md +120 -32
- data/docs/api/core/robot.md +551 -61
- data/docs/api/core/state.md +87 -197
- data/docs/api/core/tool.md +165 -20
- data/docs/api/errors.md +110 -17
- data/docs/api/hooks.md +469 -0
- data/docs/api/index.md +80 -7
- data/docs/api/mcp/client.md +129 -35
- data/docs/api/mcp/index.md +164 -23
- data/docs/api/mcp/server.md +27 -3
- data/docs/api/mcp/transports.md +94 -22
- data/docs/api/messages/index.md +26 -3
- data/docs/api/messages/text-message.md +33 -11
- data/docs/api/messages/tool-call-message.md +27 -4
- data/docs/api/messages/tool-result-message.md +23 -4
- data/docs/api/messages/user-message.md +45 -8
- data/docs/api/skills.md +519 -0
- data/docs/api/streaming/context.md +28 -5
- data/docs/api/streaming/index.md +57 -11
- data/docs/api/support.md +846 -0
- data/docs/architecture/core-concepts.md +79 -31
- data/docs/architecture/index.md +86 -11
- data/docs/architecture/message-flow.md +66 -29
- data/docs/architecture/network-orchestration.md +145 -38
- data/docs/architecture/robot-execution.md +172 -90
- data/docs/architecture/state-management.md +31 -12
- data/docs/concepts.md +176 -21
- data/docs/examples/basic-chat.md +72 -19
- data/docs/examples/index.md +117 -31
- data/docs/examples/mcp-server.md +154 -45
- data/docs/examples/multi-robot-network.md +91 -21
- data/docs/examples/tool-usage.md +104 -37
- data/docs/getting-started/configuration.md +284 -97
- data/docs/getting-started/installation.md +53 -41
- data/docs/getting-started/quick-start.md +51 -6
- data/docs/guides/building-robots.md +258 -50
- data/docs/guides/creating-networks.md +214 -30
- data/docs/guides/hooks.md +141 -54
- data/docs/guides/knowledge.md +35 -4
- data/docs/guides/mcp-integration.md +211 -44
- data/docs/guides/memory.md +103 -12
- data/docs/guides/observability.md +95 -47
- data/docs/guides/streaming.md +184 -125
- data/docs/guides/using-tools.md +237 -17
- data/docs/index.md +36 -4
- data/examples/01_simple_robot.rb +2 -2
- data/examples/02_tools.rb +14 -4
- data/examples/03_network.rb +12 -7
- data/examples/04_mcp.rb +11 -4
- data/examples/05_streaming.rb +8 -5
- data/examples/06_prompt_templates.rb +13 -9
- data/examples/07_network_memory.rb +5 -5
- data/examples/08_llm_config.rb +20 -15
- data/examples/09_chaining.rb +4 -4
- data/examples/11_network_introspection.rb +4 -4
- data/examples/12_message_bus.rb +2 -2
- data/examples/13_spawn.rb +2 -2
- data/examples/14_rusty_circuit/README.md +1 -0
- data/examples/14_rusty_circuit/comic.rb +7 -3
- data/examples/14_rusty_circuit/display.rb +14 -0
- data/examples/14_rusty_circuit/heckler.rb +8 -6
- data/examples/14_rusty_circuit/open_mic.rb +17 -6
- data/examples/14_rusty_circuit/scout.rb +17 -10
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +14 -10
- data/examples/15_memory_network_and_bus/linux_writer.rb +2 -2
- data/examples/15_memory_network_and_bus/os_editor.rb +3 -1
- data/examples/15_memory_network_and_bus/os_writer.rb +4 -1
- data/examples/16_writers_room/writer.rb +22 -22
- data/examples/16_writers_room/writers_room.rb +2 -0
- data/examples/17_skills.rb +14 -13
- data/examples/18_rails/README.md +20 -1
- data/examples/18_rails/app/controllers/chat_controller.rb +5 -1
- data/examples/18_rails/app/jobs/robot_run_job.rb +11 -5
- data/examples/18_rails/app/robots/chat_robot.rb +11 -0
- data/examples/18_rails/config/initializers/robot_lab.rb +8 -0
- data/examples/19_token_tracking.rb +25 -9
- data/examples/20_circuit_breaker.rb +10 -7
- data/examples/21_learning_loop.rb +42 -16
- data/examples/22_context_compression.rb +23 -23
- data/examples/23_convergence.rb +24 -17
- data/examples/24_structured_delegation.rb +13 -8
- data/examples/25_history_search.rb +12 -8
- data/examples/27_incident_response/incident_response.rb +31 -13
- data/examples/28_mcp_discovery.rb +17 -13
- data/examples/29_ractor_tools.rb +4 -2
- data/examples/30_ractor_network.rb +22 -17
- data/examples/31_launch_assessment.rb +20 -9
- data/examples/32_newsletter_reader.rb +7 -2
- data/examples/33_stock_predictor.rb +34 -13
- data/examples/34_agentskills.rb +7 -3
- data/examples/35_hooks.rb +18 -8
- data/examples/README.md +199 -45
- data/examples/common.rb +79 -11
- data/examples/xyzzy.rb +8 -1
- data/lib/robot_lab/config.rb +10 -5
- data/lib/robot_lab/names.rb +402 -0
- data/lib/robot_lab/robot/agent_skill_matching.rb +1 -3
- data/lib/robot_lab/robot/bus_messaging.rb +16 -8
- data/lib/robot_lab/robot/template_rendering.rb +16 -3
- data/lib/robot_lab/robot.rb +23 -2
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +21 -15
- data/mkdocs.yml +6 -1
- metadata +7 -2
|
@@ -84,6 +84,22 @@ network = RobotLab.create_network(name: "parallel_analysis") do
|
|
|
84
84
|
end
|
|
85
85
|
```
|
|
86
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
|
+
|
|
87
103
|
### Concurrency Cap
|
|
88
104
|
|
|
89
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:
|
|
@@ -107,6 +123,14 @@ end
|
|
|
107
123
|
|
|
108
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.
|
|
109
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
|
+
|
|
110
134
|
### Optional Tasks
|
|
111
135
|
|
|
112
136
|
Optional tasks only run when explicitly activated by a preceding robot:
|
|
@@ -125,6 +149,8 @@ end
|
|
|
125
149
|
Tasks can have individual context and configuration that is deep-merged with the network's run parameters:
|
|
126
150
|
|
|
127
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.
|
|
128
154
|
network = RobotLab.create_network(name: "support") do
|
|
129
155
|
task :classifier, classifier_robot, depends_on: :none
|
|
130
156
|
task :billing, billing_robot,
|
|
@@ -149,6 +175,42 @@ end
|
|
|
149
175
|
| `depends_on` | `:none`, `[:task1]`, or `:optional` |
|
|
150
176
|
| `poller_group` | Bus delivery group label (`:default`, `:slow`, etc.) |
|
|
151
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
|
+
|
|
152
214
|
## Conditional Routing
|
|
153
215
|
|
|
154
216
|
Use optional tasks with custom Robot subclasses for intelligent routing:
|
|
@@ -192,6 +254,13 @@ end
|
|
|
192
254
|
|
|
193
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.
|
|
194
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
|
+
|
|
195
264
|
Named **poller groups** let you label tasks so slow robots are identifiable in logs and monitoring without needing separate infrastructure:
|
|
196
265
|
|
|
197
266
|
```ruby
|
|
@@ -234,7 +303,7 @@ result = network.run(
|
|
|
234
303
|
```ruby
|
|
235
304
|
result = network.run(message: "Process this")
|
|
236
305
|
|
|
237
|
-
# Access individual robot results
|
|
306
|
+
# Access individual robot results — keyed by the ROBOT's name
|
|
238
307
|
classifier_result = result.context[:classifier]
|
|
239
308
|
billing_result = result.context[:billing]
|
|
240
309
|
|
|
@@ -242,6 +311,14 @@ billing_result = result.context[:billing]
|
|
|
242
311
|
original_params = result.context[:run_params]
|
|
243
312
|
```
|
|
244
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
|
+
|
|
245
322
|
## SimpleFlow::Result
|
|
246
323
|
|
|
247
324
|
Networks return a `SimpleFlow::Result` object:
|
|
@@ -249,12 +326,24 @@ Networks return a `SimpleFlow::Result` object:
|
|
|
249
326
|
```ruby
|
|
250
327
|
result = network.run(message: "Hello")
|
|
251
328
|
|
|
252
|
-
result.value
|
|
253
|
-
result.context
|
|
254
|
-
result.
|
|
255
|
-
result.
|
|
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
|
|
256
334
|
```
|
|
257
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
|
+
|
|
258
347
|
## Broadcasting
|
|
259
348
|
|
|
260
349
|
Networks support a broadcast channel for network-wide announcements:
|
|
@@ -341,7 +430,7 @@ When a robot raises an exception during pipeline execution, the error is caught
|
|
|
341
430
|
|
|
342
431
|
```ruby
|
|
343
432
|
# If billing_robot raises an error, the network continues
|
|
344
|
-
# The error is available in the result context:
|
|
433
|
+
# The error is available in the result context, keyed by the robot's name:
|
|
345
434
|
result = network.run(message: "Process this")
|
|
346
435
|
billing_result = result.context[:billing]
|
|
347
436
|
|
|
@@ -384,7 +473,7 @@ Access previous task results via context:
|
|
|
384
473
|
```ruby
|
|
385
474
|
class ResponderRobot < RobotLab::Robot
|
|
386
475
|
def call(result)
|
|
387
|
-
# Get classifier's output
|
|
476
|
+
# Get classifier's output — the key is the classifier ROBOT's name
|
|
388
477
|
classification = result.context[:classifier]&.last_text_content
|
|
389
478
|
|
|
390
479
|
context = extract_run_context(result)
|
|
@@ -435,10 +524,11 @@ puts network.execution_plan
|
|
|
435
524
|
|
|
436
525
|
```ruby
|
|
437
526
|
network.name # => "support"
|
|
438
|
-
network.robots # => Hash of
|
|
527
|
+
network.robots # => Hash of key => Robot (see the key caveat below)
|
|
439
528
|
network.robot(:billing) # => Robot instance
|
|
440
529
|
network["billing"] # => Robot instance (alias)
|
|
441
530
|
network.available_robots # => Array of Robot instances
|
|
531
|
+
network.crew # => Array of Robot instances (Runnable protocol)
|
|
442
532
|
network.memory # => Memory instance (shared)
|
|
443
533
|
network.to_h # => Hash representation
|
|
444
534
|
|
|
@@ -446,55 +536,149 @@ network.add_robot(extra_robot) # add without a pipeline task -> self
|
|
|
446
536
|
network.remove_robot(:extra_robot) # remove by name -> the removed Robot, or nil
|
|
447
537
|
```
|
|
448
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
|
+
|
|
449
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.
|
|
450
552
|
|
|
451
553
|
## Configuration Inheritance
|
|
452
554
|
|
|
453
|
-
Networks accept a `config:` parameter
|
|
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)).
|
|
454
570
|
|
|
455
|
-
### Network-Wide Defaults
|
|
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.
|
|
456
581
|
|
|
457
582
|
```ruby
|
|
458
|
-
|
|
459
|
-
|
|
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
|
+
)
|
|
460
597
|
|
|
461
598
|
network = RobotLab.create_network(name: "pipeline", config: shared) do
|
|
462
|
-
task :analyzer, analyzer_robot, depends_on: :none
|
|
463
|
-
task :writer,
|
|
464
|
-
task :reviewer, reviewer_robot, depends_on: [:writer]
|
|
599
|
+
task :analyzer, analyzer_robot, depends_on: :none, tools: :inherit, mcp: :inherit
|
|
600
|
+
task :writer, writer_robot, depends_on: [:analyzer]
|
|
465
601
|
end
|
|
466
602
|
```
|
|
467
603
|
|
|
468
|
-
|
|
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`) | `[]` |
|
|
469
611
|
|
|
470
|
-
|
|
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:
|
|
471
642
|
|
|
472
643
|
```ruby
|
|
473
|
-
|
|
644
|
+
MODEL = "claude-sonnet-4"
|
|
474
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.
|
|
475
657
|
network = RobotLab.create_network(name: "pipeline", config: shared) do
|
|
476
|
-
task :analyzer, analyzer_robot, depends_on: :none
|
|
658
|
+
task :analyzer, analyzer_robot, depends_on: :none, tools: :inherit
|
|
477
659
|
task :writer, writer_robot,
|
|
478
|
-
config:
|
|
660
|
+
config: RobotLab::RunConfig.new(tools: [DraftTool]), # tools/mcp only
|
|
661
|
+
tools: :inherit,
|
|
479
662
|
depends_on: [:analyzer]
|
|
480
|
-
task :reviewer, reviewer_robot, depends_on: [:writer]
|
|
481
663
|
end
|
|
482
664
|
```
|
|
483
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
|
+
|
|
484
669
|
### Inheritance Chain
|
|
485
670
|
|
|
486
|
-
|
|
671
|
+
Per robot, the cascade runs least- to most-specific — note that template front matter is the **base**, not an override:
|
|
487
672
|
|
|
488
673
|
```
|
|
489
|
-
|
|
490
|
-
->
|
|
491
|
-
->
|
|
492
|
-
-> Robot config (from constructor)
|
|
493
|
-
-> Template front matter
|
|
494
|
-
-> Constructor kwargs (model:, temperature:, etc.)
|
|
674
|
+
template front matter (lowest — the starting point)
|
|
675
|
+
-> config: RunConfig
|
|
676
|
+
-> constructor kwargs (highest — always wins)
|
|
495
677
|
```
|
|
496
678
|
|
|
497
|
-
|
|
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.
|
|
498
682
|
|
|
499
683
|
## Best Practices
|
|
500
684
|
|
|
@@ -527,7 +711,7 @@ Guard against missing optional task results:
|
|
|
527
711
|
|
|
528
712
|
```ruby
|
|
529
713
|
def call(result)
|
|
530
|
-
# Check if optional task ran
|
|
714
|
+
# Check if the optional task ran (key = that robot's name)
|
|
531
715
|
if result.context[:validator]
|
|
532
716
|
# Use validator result
|
|
533
717
|
else
|