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
data/docs/api/core/network.md
CHANGED
|
@@ -11,6 +11,17 @@ network = RobotLab.create_network(name: "support", config: config) do
|
|
|
11
11
|
end
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
`RobotLab.create_network(name:, concurrency: :auto, config: nil, &block)` is the
|
|
15
|
+
factory. `Network.new` additionally accepts `memory:` and `parallel_mode:`:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
RobotLab::Network.new(name:, concurrency: :auto, memory: nil, config: nil, parallel_mode: :async, &block)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
There is no `router:` and no `robots:` keyword. Routing is done by subclassing
|
|
22
|
+
`Robot`, overriding `#call`, and activating optional tasks — see
|
|
23
|
+
[Conditional Routing](#conditional-routing).
|
|
24
|
+
|
|
14
25
|
## Attributes
|
|
15
26
|
|
|
16
27
|
### name
|
|
@@ -27,7 +38,37 @@ Network identifier for logging and debugging.
|
|
|
27
38
|
network.robots # => Hash<String, Robot>
|
|
28
39
|
```
|
|
29
40
|
|
|
30
|
-
|
|
41
|
+
Robots keyed by **String**. The key depends on how the robot was registered:
|
|
42
|
+
|
|
43
|
+
| Registered via | Key |
|
|
44
|
+
|----------------|-----|
|
|
45
|
+
| `task(:analyzer, robot)` | the **task** name — `"analyzer"` — regardless of `robot.name` |
|
|
46
|
+
| `add_robot(robot)` | `robot.name` |
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
n = RobotLab.create_network(name: "n") do
|
|
50
|
+
task :a, RobotLab.build(name: "alpha"), depends_on: :none
|
|
51
|
+
end
|
|
52
|
+
n.add_robot(RobotLab.build(name: "helper"))
|
|
53
|
+
|
|
54
|
+
n.robots.keys # => ["a", "helper"]
|
|
55
|
+
n.crew.map(&:name) # => ["alpha", "helper"]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Examples that assume the two coincide only work when you name the robot after
|
|
59
|
+
its task. `result.context` is keyed by the **robot's** name (`@name.to_sym`), not
|
|
60
|
+
the task name — another reason to keep them identical.
|
|
61
|
+
|
|
62
|
+
### memory
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
network.memory # => Memory
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Shared reactive memory for every robot in the network. It is passed to each robot
|
|
69
|
+
on `run()` as `network_memory:`, so in-network robots read and write this instance
|
|
70
|
+
instead of their own inherent memory. Created as `Memory.new(network_name: name)`
|
|
71
|
+
unless one is supplied to `Network.new(memory:)`.
|
|
31
72
|
|
|
32
73
|
### config
|
|
33
74
|
|
|
@@ -35,7 +76,35 @@ Hash of robots keyed by name.
|
|
|
35
76
|
network.config # => RunConfig
|
|
36
77
|
```
|
|
37
78
|
|
|
38
|
-
Shared operational defaults
|
|
79
|
+
Shared operational defaults. Passed to robots during `run()` as `network_config:`.
|
|
80
|
+
|
|
81
|
+
!!! warning "Only `mcp` and `tools` propagate to member robots"
|
|
82
|
+
LLM fields (`model`, `temperature`, `max_tokens`, …) and callbacks
|
|
83
|
+
(`on_content`, `on_tool_call`, `on_tool_result`) are read from each robot's
|
|
84
|
+
own config at construction time and are **never** inherited from the
|
|
85
|
+
network. A member robot picks up the network's `mcp`/`tools` only when it
|
|
86
|
+
opts in with `:inherit`. `max_concurrent_robots` is the one field the
|
|
87
|
+
network itself consumes (it is passed to `pipeline.call_parallel`).
|
|
88
|
+
|
|
89
|
+
### parallel_mode
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
network.parallel_mode # => :async (default) or :ractor
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Execution strategy for `run`. `:async` uses the SimpleFlow pipeline;
|
|
96
|
+
`:ractor` routes through `RactorNetworkScheduler` and **raises
|
|
97
|
+
`RobotLab::DependencyError`** unless the `robot_lab-ractor` gem is loaded. Set via
|
|
98
|
+
`Network.new(parallel_mode:)`.
|
|
99
|
+
|
|
100
|
+
### hooks
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
network.hooks # => RobotLab::HookRegistry
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The network's own hook registry, populated by [`network.on`](#on). Consulted for
|
|
107
|
+
network-run, robot-run, and task hooks alongside `RobotLab.hooks`.
|
|
39
108
|
|
|
40
109
|
### pipeline
|
|
41
110
|
|
|
@@ -70,12 +139,25 @@ Execute the network pipeline.
|
|
|
70
139
|
| `message` | `String`, `nil` | The input message — as the `message:` keyword, or positionally (folded into `message:` when given) |
|
|
71
140
|
| `**context` | `Hash` | Additional context passed to all robots |
|
|
72
141
|
|
|
73
|
-
|
|
142
|
+
`run` injects three keys into the run context before executing:
|
|
143
|
+
`network_memory:` (the shared `Memory`), `network:` (the network itself), and
|
|
144
|
+
`network_config:` (only when the network's `RunConfig` is non-empty). The whole
|
|
145
|
+
execution is wrapped in the `:network_run` hook family.
|
|
146
|
+
|
|
147
|
+
**Returns:** `SimpleFlow::Result` — except under `parallel_mode: :ractor`, where
|
|
148
|
+
`RactorNetworkScheduler#run_pipeline` returns its own results structure.
|
|
149
|
+
|
|
150
|
+
!!! note "Member robots still default to `tools: :none`"
|
|
151
|
+
Task-level `tools:`/`mcp:` default to `:none` just like `Robot#run`. Declare
|
|
152
|
+
the task with `tools: :inherit` (or an explicit name array) for its robot to
|
|
153
|
+
receive any tools.
|
|
74
154
|
|
|
75
155
|
### task
|
|
76
156
|
|
|
77
157
|
```ruby
|
|
78
|
-
network.task(name, robot,
|
|
158
|
+
network.task(name, robot,
|
|
159
|
+
context: {}, mcp: :none, tools: :none, memory: nil,
|
|
160
|
+
config: nil, depends_on: :none, poller_group: :default)
|
|
79
161
|
# => self
|
|
80
162
|
```
|
|
81
163
|
|
|
@@ -83,16 +165,17 @@ Add a task to the pipeline with optional per-task configuration.
|
|
|
83
165
|
|
|
84
166
|
**Parameters:**
|
|
85
167
|
|
|
86
|
-
| Name | Type | Description |
|
|
87
|
-
|
|
88
|
-
| `name` | `Symbol` | Task identifier |
|
|
89
|
-
| `robot` | `Robot` | Robot instance to execute |
|
|
90
|
-
| `context` | `Hash` | Task-specific context (deep-merged with run params) |
|
|
91
|
-
| `mcp` |
|
|
92
|
-
| `tools` |
|
|
93
|
-
| `memory` | `Memory`, `nil` | Task-specific memory |
|
|
94
|
-
| `config` | `RunConfig`, `nil` | Per-task config
|
|
95
|
-
| `depends_on` | `:none`, `Array<Symbol>`, `:optional` | Task dependencies |
|
|
168
|
+
| Name | Type | Default | Description |
|
|
169
|
+
|------|------|---------|-------------|
|
|
170
|
+
| `name` | `Symbol` | **required** | Task identifier; also the key under which the robot is stored in `network.robots` |
|
|
171
|
+
| `robot` | `Robot` | **required** | Robot instance to execute |
|
|
172
|
+
| `context` | `Hash` | `{}` | Task-specific context (deep-merged with run params) |
|
|
173
|
+
| `mcp` | `Symbol`, `Array` | `:none` | MCP server config for this task |
|
|
174
|
+
| `tools` | `Symbol`, `Array` | `:none` | Tools config for this task — tool **names**, not instances |
|
|
175
|
+
| `memory` | `Memory`, `Hash`, `nil` | `nil` | Task-specific memory, overriding the network's shared memory |
|
|
176
|
+
| `config` | `RunConfig`, `nil` | `nil` | Per-task config, merged on top of the network's RunConfig. Like the network config, only `mcp`/`tools` reach the robot |
|
|
177
|
+
| `depends_on` | `:none`, `Array<Symbol>`, `:optional` | `:none` | Task dependencies |
|
|
178
|
+
| `poller_group` | `Symbol` | `:default` | Bus-poller group for this robot; the network registers the group on its shared `BusPoller` and assigns it to the robot |
|
|
96
179
|
|
|
97
180
|
**Dependency Types:**
|
|
98
181
|
|
|
@@ -100,7 +183,106 @@ Add a task to the pipeline with optional per-task configuration.
|
|
|
100
183
|
|-------|-------------|
|
|
101
184
|
| `:none` | No dependencies, runs first |
|
|
102
185
|
| `[:task1, :task2]` | Waits for listed tasks to complete |
|
|
103
|
-
| `:optional` | Only runs when explicitly activated |
|
|
186
|
+
| `:optional` | Only runs when explicitly activated via `result.activate(:name)` |
|
|
187
|
+
|
|
188
|
+
### parallel
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
network.parallel(name = nil, depends_on: :none) { ... }
|
|
192
|
+
# => self
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Declare a named group of steps that run concurrently, then depend on the group as
|
|
196
|
+
a unit. Delegates directly to `SimpleFlow::Pipeline#parallel`.
|
|
197
|
+
|
|
198
|
+
**Parameters:**
|
|
199
|
+
|
|
200
|
+
| Name | Type | Default | Description |
|
|
201
|
+
|------|------|---------|-------------|
|
|
202
|
+
| `name` | `Symbol`, `nil` | `nil` | Optional name for the group, usable in a later `depends_on:` |
|
|
203
|
+
| `depends_on` | `Symbol`, `Array` | `:none` | Dependencies for the group as a whole |
|
|
204
|
+
|
|
205
|
+
!!! warning "Use `step` inside the block, not `task`"
|
|
206
|
+
The block is `instance_eval`'d by `SimpleFlow::Pipeline::ParallelBlock`,
|
|
207
|
+
whose only DSL methods are `step` and `steps` — **not** `task`. This is an
|
|
208
|
+
inconsistency with the top-level `Network` DSL. Inner entries therefore also
|
|
209
|
+
bypass `Network#task`, so they are not registered in `network.robots`, get no
|
|
210
|
+
per-task `context:`/`tools:`/`memory:`, and are not assigned a poller group.
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
network.parallel :fetch_data, depends_on: :validate do
|
|
214
|
+
step :fetch_orders, orders_robot
|
|
215
|
+
step :fetch_products, products_robot
|
|
216
|
+
end
|
|
217
|
+
network.task :process, processor, depends_on: :fetch_data
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Declaring the concurrent robots as ordinary `task`s that share a `depends_on:`
|
|
221
|
+
avoids all of that and is the pattern used throughout these docs.
|
|
222
|
+
|
|
223
|
+
### broadcast
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
network.broadcast(payload)
|
|
227
|
+
# => self
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Send a network-wide announcement. Wraps `payload` as
|
|
231
|
+
`{ payload:, network:, timestamp: }`, dispatches it asynchronously to every
|
|
232
|
+
handler registered with [`on_broadcast`](#on_broadcast), and also writes it to
|
|
233
|
+
shared memory under `Network::BROADCAST_KEY` (`:_network_broadcast`) so robots can
|
|
234
|
+
pick it up with `memory.subscribe(:_network_broadcast)`.
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
network.broadcast(event: :pause, reason: "rate limit hit")
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### on_broadcast
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
network.on_broadcast { |message| ... }
|
|
244
|
+
# => self
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Register a handler for `broadcast` messages. The block receives the full envelope
|
|
248
|
+
(`message[:payload]`, `message[:network]`, `message[:timestamp]`).
|
|
249
|
+
|
|
250
|
+
**Raises:** `ArgumentError` if no block is given.
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
network.on_broadcast do |message|
|
|
254
|
+
pause_current_work if message[:payload][:event] == :pause
|
|
255
|
+
end
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### reset_memory
|
|
259
|
+
|
|
260
|
+
```ruby
|
|
261
|
+
network.reset_memory
|
|
262
|
+
# => self
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Reset the network's shared memory to its initial state (`Memory#reset`). Useful
|
|
266
|
+
between runs. This clears the key-value store only; it does not touch any robot's
|
|
267
|
+
chat history.
|
|
268
|
+
|
|
269
|
+
### on
|
|
270
|
+
|
|
271
|
+
```ruby
|
|
272
|
+
network.on(HandlerClass, context: nil)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Register a hook handler on the network's own registry (`network.hooks`). Unlike
|
|
276
|
+
`robot.on`, handlers registered here **do** fire for the `:task` hook family,
|
|
277
|
+
which resolves against `[RobotLab.hooks, network&.hooks]`.
|
|
278
|
+
|
|
279
|
+
### to_dot
|
|
280
|
+
|
|
281
|
+
```ruby
|
|
282
|
+
network.to_dot # => String, or nil
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Graphviz DOT representation of the pipeline (`pipeline.visualize_dot`).
|
|
104
286
|
|
|
105
287
|
### add_robot
|
|
106
288
|
|
|
@@ -172,34 +354,58 @@ Hash representation of network configuration.
|
|
|
172
354
|
```ruby
|
|
173
355
|
{
|
|
174
356
|
name: "support",
|
|
175
|
-
robots: ["classifier", "billing", "technical"],
|
|
176
|
-
tasks: ["classifier", "billing", "technical"],
|
|
357
|
+
robots: ["classifier", "billing", "technical"], # keys of network.robots
|
|
358
|
+
tasks: ["classifier", "billing", "technical"], # task names only
|
|
177
359
|
optional_tasks: [:billing, :technical],
|
|
178
|
-
config: { model: "claude-sonnet-4", temperature: 0.7 }
|
|
360
|
+
config: { model: "claude-sonnet-4", temperature: 0.7 }
|
|
179
361
|
}
|
|
180
362
|
```
|
|
181
363
|
|
|
182
|
-
|
|
364
|
+
The hash is `.compact`ed and `config` is omitted entirely when the network's
|
|
365
|
+
`RunConfig` is empty. `config` comes from `RunConfig#to_json_hash`, so the
|
|
366
|
+
non-serializable fields (`on_tool_call`, `on_tool_result`, `on_content`, `bus`,
|
|
367
|
+
`auto_compact`) are excluded. `robots` includes robots added with `add_robot`;
|
|
368
|
+
`tasks` does not.
|
|
183
369
|
|
|
184
|
-
|
|
370
|
+
## SimpleFlow::Result
|
|
185
371
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
372
|
+
`Network#run` returns a `SimpleFlow::Result`. This is its **complete** public API
|
|
373
|
+
(simple_flow 0.4):
|
|
374
|
+
|
|
375
|
+
| Method | Description |
|
|
376
|
+
|--------|-------------|
|
|
377
|
+
| `value` | The final task's output (a `RobotResult`) |
|
|
378
|
+
| `context` | Hash of accumulated context, including every task's result |
|
|
379
|
+
| `continue?` | `true` while the pipeline is still running steps |
|
|
380
|
+
| `continue(value)` | Returns a new Result carrying `value`, still continuing |
|
|
381
|
+
| `halt(value)` | Returns a new Result that stops the pipeline |
|
|
382
|
+
| `with_context(key, value)` | Returns a new Result with an added context entry |
|
|
383
|
+
| `with_error(key, message)` | Returns a new Result with an added error — **both arguments are required** |
|
|
384
|
+
| `errors` | Accumulated errors |
|
|
385
|
+
| `activate(step_name)` | Marks an `:optional` step to run |
|
|
386
|
+
| `activated_steps` | The set of optional steps activated so far |
|
|
387
|
+
|
|
388
|
+
!!! danger "`halted?`, `continued?`, and `with_value` do not exist"
|
|
389
|
+
They raise `NoMethodError`. The predicate is `continue?` — a halted result
|
|
390
|
+
is one where `continue?` is `false`.
|
|
391
|
+
|
|
392
|
+
```ruby
|
|
393
|
+
result = network.run(message: "Hello")
|
|
394
|
+
puts "stopped early" unless result.continue?
|
|
395
|
+
```
|
|
194
396
|
|
|
195
397
|
### Context Structure
|
|
196
398
|
|
|
197
399
|
```ruby
|
|
198
400
|
result.context[:run_params] # Original run parameters
|
|
199
|
-
result.context[:classifier] #
|
|
200
|
-
result.context[:billing] #
|
|
401
|
+
result.context[:classifier] # RobotResult from the robot NAMED "classifier"
|
|
402
|
+
result.context[:billing] # RobotResult from the robot NAMED "billing"
|
|
201
403
|
```
|
|
202
404
|
|
|
405
|
+
`Robot#call` writes its result with `result.with_context(@name.to_sym, robot_result)`,
|
|
406
|
+
so context keys are **robot names**, not task names. Name each robot after its
|
|
407
|
+
task to keep the two aligned.
|
|
408
|
+
|
|
203
409
|
## Builder DSL
|
|
204
410
|
|
|
205
411
|
### task
|
|
@@ -222,11 +428,17 @@ network = RobotLab.create_network(name: "support") do
|
|
|
222
428
|
depends_on: :optional
|
|
223
429
|
task :technical, technical_robot,
|
|
224
430
|
context: { department: "technical" },
|
|
225
|
-
|
|
431
|
+
# Entries must match how the robot attached each tool. If technical_robot
|
|
432
|
+
# was built with local_tools: [DebugTool, LogTool] (classes), use the class
|
|
433
|
+
# names; if it attached instances, use %w[debug log] instead.
|
|
434
|
+
tools: %w[DebugTool LogTool],
|
|
226
435
|
depends_on: :optional
|
|
227
436
|
end
|
|
228
437
|
```
|
|
229
438
|
|
|
439
|
+
`tools:` here is a **name allowlist**, exactly as on `Robot`. Attach the tool
|
|
440
|
+
objects themselves with `local_tools:` when building the robot.
|
|
441
|
+
|
|
230
442
|
## Examples
|
|
231
443
|
|
|
232
444
|
### Sequential Pipeline
|
|
@@ -259,10 +471,16 @@ end
|
|
|
259
471
|
|
|
260
472
|
### Conditional Routing
|
|
261
473
|
|
|
474
|
+
There is no `Router` class and no `Router::Args`. Conditional routing is done by
|
|
475
|
+
subclassing `Robot`, overriding `#call`, and calling `result.activate(:task_name)`
|
|
476
|
+
for tasks declared `depends_on: :optional`:
|
|
477
|
+
|
|
262
478
|
```ruby
|
|
263
479
|
class ClassifierRobot < RobotLab::Robot
|
|
264
480
|
def call(result)
|
|
265
|
-
|
|
481
|
+
run_context = extract_run_context(result)
|
|
482
|
+
message = run_context.delete(:message) # must be positional
|
|
483
|
+
robot_result = run(message, **run_context)
|
|
266
484
|
|
|
267
485
|
new_result = result
|
|
268
486
|
.with_context(@name.to_sym, robot_result)
|
|
@@ -319,11 +537,15 @@ end
|
|
|
319
537
|
| `single?` | `false` |
|
|
320
538
|
|
|
321
539
|
```ruby
|
|
322
|
-
network.crew.map(&:name) # =>
|
|
540
|
+
network.crew.map(&:name) # => the ROBOTS' names, not the task keys
|
|
323
541
|
network.chief # => the first robot
|
|
324
542
|
network.network? # => true
|
|
325
543
|
```
|
|
326
544
|
|
|
545
|
+
`crew` returns `robots.values` — robot *instances*. So `crew.map(&:name)` yields
|
|
546
|
+
`robot.name` for each, which differs from `network.robots.keys` (task names)
|
|
547
|
+
unless each robot is named after its task.
|
|
548
|
+
|
|
327
549
|
## See Also
|
|
328
550
|
|
|
329
551
|
- [Creating Networks Guide](../../guides/creating-networks.md)
|
data/docs/api/core/result.md
CHANGED
|
@@ -8,12 +8,34 @@
|
|
|
8
8
|
result = robot.run("What is the capital of France?")
|
|
9
9
|
|
|
10
10
|
result.reply # => "The capital of France is Paris."
|
|
11
|
-
result.last_text_content # => alias for
|
|
12
|
-
result.output # =>
|
|
13
|
-
result.tool_calls # =>
|
|
11
|
+
result.last_text_content # => reply is an alias for this
|
|
12
|
+
result.output # => [TextMessage] — see below
|
|
13
|
+
result.tool_calls # => [] in practice — see below
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
`
|
|
16
|
+
`last_text_content` returns the content of the last text message in `output`;
|
|
17
|
+
`reply` is an alias for it. This is the string you want for the vast majority of
|
|
18
|
+
use cases.
|
|
19
|
+
|
|
20
|
+
!!! note "`output` is not the full turn"
|
|
21
|
+
`output` is built as `[TextMessage.new(role: "assistant", content: text)]`
|
|
22
|
+
from the **final response text only** — a one-element array, or an empty
|
|
23
|
+
array when there is no text. It never contains the user message, the tool
|
|
24
|
+
calls, or the intermediate assistant turns. Read `robot.messages` for the
|
|
25
|
+
real conversation.
|
|
26
|
+
|
|
27
|
+
The text is resolved in this order: `response.content`; then
|
|
28
|
+
`response.thinking.text` (for models that route all output through reasoning
|
|
29
|
+
content, e.g. `qwen3` on Ollama); then the most recent assistant text from
|
|
30
|
+
*the current turn only* in the chat history.
|
|
31
|
+
|
|
32
|
+
!!! note "`tool_calls` is effectively always empty"
|
|
33
|
+
It is populated from the **final** assistant message, which no longer carries
|
|
34
|
+
tool calls once ruby_llm's tool loop has completed. Consequently
|
|
35
|
+
`has_tool_calls?` is also normally `false`. To observe tool activity, use the
|
|
36
|
+
`on_tool_call:` / `on_tool_result:` callbacks or the `:tool_call` hook family.
|
|
37
|
+
|
|
38
|
+
There is **no** `text?` predicate and **no** `content` accessor on `RobotResult`.
|
|
17
39
|
|
|
18
40
|
## Token & Cost Tracking
|
|
19
41
|
|
|
@@ -47,77 +69,143 @@ result.duration # => 2.34 (always set by delegate)
|
|
|
47
69
|
result.robot_name # => "analyst"
|
|
48
70
|
result.id # => "550e8400-e29b-..." (UUID, unique per run)
|
|
49
71
|
result.created_at # => Time instance
|
|
50
|
-
result.stop_reason # =>
|
|
72
|
+
result.stop_reason # => nil — always, for results built by Robot#run
|
|
51
73
|
```
|
|
52
74
|
|
|
53
|
-
|
|
75
|
+
!!! warning "`stop_reason` is always `nil` here"
|
|
76
|
+
`build_result` sets it with
|
|
77
|
+
`response.respond_to?(:stop_reason) ? response.stop_reason : nil`, and
|
|
78
|
+
`RubyLLM::Message` does **not** define `stop_reason`. So every
|
|
79
|
+
`robot.run` result carries `nil`, and `export` drops the key entirely.
|
|
80
|
+
(`RobotLab::Message::VALID_STOP_REASONS` is `["tool", "stop"]`, but that
|
|
81
|
+
constant governs the message classes you construct yourself — it is not
|
|
82
|
+
what a `RobotResult` reports.)
|
|
83
|
+
|
|
84
|
+
## Inspecting the Output
|
|
54
85
|
|
|
55
86
|
```ruby
|
|
56
87
|
result.output.each do |message|
|
|
57
|
-
puts message.role #
|
|
58
|
-
puts message.content # String
|
|
88
|
+
puts message.role # "assistant"
|
|
89
|
+
puts message.content # String
|
|
59
90
|
end
|
|
60
91
|
|
|
61
|
-
result.has_tool_calls? # =>
|
|
92
|
+
result.has_tool_calls? # => output.any?(&:tool_call?) || tool_calls.any?
|
|
62
93
|
result.stopped? # => true if execution ended naturally (not mid-tool-call)
|
|
63
94
|
```
|
|
64
95
|
|
|
96
|
+
Because `stop_reason` is always `nil` on a `Robot#run` result, `stopped?`
|
|
97
|
+
collapses to `!has_tool_calls?` in practice — and since `tool_calls` is
|
|
98
|
+
effectively always empty, it is normally `true`.
|
|
99
|
+
|
|
65
100
|
## Persistence
|
|
66
101
|
|
|
67
|
-
Export for serialization (excludes debug fields):
|
|
102
|
+
Export for serialization (excludes the debug fields):
|
|
68
103
|
|
|
69
104
|
```ruby
|
|
70
105
|
hash = result.export
|
|
71
106
|
# {
|
|
72
107
|
# robot_name: "analyst",
|
|
108
|
+
# delegated_by: "manager",
|
|
73
109
|
# output: [...],
|
|
74
110
|
# tool_calls: [...],
|
|
75
111
|
# created_at: "2026-04-18T12:00:00Z",
|
|
76
112
|
# id: "550e8400-...",
|
|
77
113
|
# checksum: "a1b2c3...",
|
|
78
|
-
# stop_reason
|
|
114
|
+
# # stop_reason is absent — it is nil, and export is .compact-ed
|
|
79
115
|
# duration: 2.34,
|
|
80
116
|
# input_tokens: 512,
|
|
81
117
|
# output_tokens: 128
|
|
82
118
|
# }
|
|
83
119
|
|
|
84
|
-
json = result.to_json
|
|
120
|
+
json = result.to_json # uses export
|
|
121
|
+
|
|
122
|
+
hash_with_debug = result.to_h # export + prompt/history/raw, also compacted
|
|
85
123
|
|
|
86
124
|
# Reconstruct from hash
|
|
87
125
|
restored = RobotLab::RobotResult.from_hash(hash)
|
|
88
126
|
```
|
|
89
127
|
|
|
90
|
-
`
|
|
128
|
+
!!! warning "`export` is `.compact`ed"
|
|
129
|
+
Keys whose value is `nil` are dropped entirely. `delegated_by` and
|
|
130
|
+
`duration` disappear when unset, and `stop_reason` is dropped from
|
|
131
|
+
**every** `Robot#run` result because it is always `nil`. `input_tokens` and
|
|
132
|
+
`output_tokens` are additionally converted to `nil` (and therefore dropped)
|
|
133
|
+
when they are **zero**, so a provider that reports no usage yields a hash
|
|
134
|
+
with no token keys at all:
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
RobotLab::RobotResult.new(robot_name: "a", output: []).export
|
|
138
|
+
# => { robot_name: "a", output: [], tool_calls: [],
|
|
139
|
+
# created_at: "...", id: "...", checksum: "..." }
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Only `robot_name`, `output`, `tool_calls`, `created_at`, `id`, and `checksum`
|
|
143
|
+
are always present. Code reading an exported hash must tolerate missing keys.
|
|
144
|
+
|
|
145
|
+
`checksum` is a SHA-256 hex digest of `{ output:, tool_calls:, created_at: }`
|
|
146
|
+
(with `created_at` reduced to an integer epoch). Use it for deduplication when
|
|
147
|
+
persisting results.
|
|
91
148
|
|
|
92
149
|
## Debug Fields
|
|
93
150
|
|
|
94
|
-
|
|
151
|
+
All three are read/write and excluded from `export`/`to_json` (but included in `to_h`):
|
|
95
152
|
|
|
96
153
|
```ruby
|
|
97
|
-
result.prompt # Array<Message
|
|
98
|
-
result.history # Array<Message
|
|
99
|
-
result.raw # raw
|
|
154
|
+
result.prompt # Array<Message>, nil — nil unless you assign it
|
|
155
|
+
result.history # Array<Message>, nil — nil unless you assign it
|
|
156
|
+
result.raw # the raw ruby_llm response; Robot#run always sets this
|
|
100
157
|
```
|
|
101
158
|
|
|
159
|
+
`raw` is populated by `Robot#build_result` on every run, so it is the escape
|
|
160
|
+
hatch for anything RobotLab does not surface (cost, provider metadata, the
|
|
161
|
+
untouched message object). `prompt` and `history` are never populated by the
|
|
162
|
+
framework.
|
|
163
|
+
|
|
102
164
|
## Attribute Reference
|
|
103
165
|
|
|
104
|
-
| Attribute | Type | Description |
|
|
105
|
-
|
|
106
|
-
| `robot_name` | String | Name of the robot that produced this result |
|
|
107
|
-
| `
|
|
108
|
-
| `output` | Array\<
|
|
109
|
-
| `tool_calls` | Array\<ToolResultMessage\> | Tool call results |
|
|
110
|
-
| `input_tokens` | Integer | Tokens sent to LLM |
|
|
111
|
-
| `output_tokens` | Integer | Tokens generated |
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
117
|
-
| `
|
|
166
|
+
| Attribute | Access | Type | Description |
|
|
167
|
+
|-----------|--------|------|-------------|
|
|
168
|
+
| `robot_name` | r | String | Name of the robot that produced this result |
|
|
169
|
+
| `last_text_content` | r | String, nil | Content of the last text message in `output` (alias: `reply`) |
|
|
170
|
+
| `output` | r | Array\<TextMessage\> | The final response text, as a one-element array (or empty) |
|
|
171
|
+
| `tool_calls` | r | Array\<ToolResultMessage\> | Tool call results — effectively always empty |
|
|
172
|
+
| `input_tokens` | r | Integer | Tokens sent to LLM (0 when the provider reports no usage) |
|
|
173
|
+
| `output_tokens` | r | Integer | Tokens generated (0 when not reported) |
|
|
174
|
+
| `id` | r | String | UUID, generated per result |
|
|
175
|
+
| `created_at` | r | Time | Creation timestamp |
|
|
176
|
+
| `stop_reason` | r | String, nil | Always `nil` for `Robot#run` results (the ruby_llm response has no `stop_reason`); settable only when you build a `RobotResult` yourself |
|
|
177
|
+
| `duration` | rw | Float, nil | Elapsed seconds — set by `delegate` and by `Robot#call` in a pipeline; `nil` for a direct `robot.run` |
|
|
178
|
+
| `delegated_by` | rw | String, nil | Delegating robot's name — set by `delegate` |
|
|
179
|
+
| `prompt` | rw | Array\<Message\>, nil | Debug field, `nil` unless assigned |
|
|
180
|
+
| `history` | rw | Array\<Message\>, nil | Debug field, `nil` unless assigned |
|
|
181
|
+
| `raw` | rw | Object, nil | The raw ruby_llm response; set by `Robot#build_result` |
|
|
182
|
+
|
|
183
|
+
Methods: `checksum`, `export`, `to_h`, `to_json`, `has_tool_calls?`, `stopped?`,
|
|
184
|
+
and the class method `RobotResult.from_hash`.
|
|
185
|
+
|
|
186
|
+
## Constructor
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
RobotLab::RobotResult.new(
|
|
190
|
+
robot_name:, # required
|
|
191
|
+
output:, # required — Array<Message, Hash>
|
|
192
|
+
tool_calls: [],
|
|
193
|
+
created_at: nil, # defaults to Time.now
|
|
194
|
+
id: nil, # defaults to SecureRandom.uuid
|
|
195
|
+
prompt: nil,
|
|
196
|
+
history: nil,
|
|
197
|
+
raw: nil,
|
|
198
|
+
stop_reason: nil,
|
|
199
|
+
input_tokens: 0,
|
|
200
|
+
output_tokens: 0
|
|
201
|
+
)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`output` entries must be `Message` instances or Hashes (`Message.from_hash`);
|
|
205
|
+
anything else raises `ArgumentError`.
|
|
118
206
|
|
|
119
207
|
## Related
|
|
120
208
|
|
|
121
|
-
- [Robot API](robot.md) — `run
|
|
209
|
+
- [Robot API](robot.md) — [`run`](robot.md#run), [`delegate`](robot.md#delegate), [`compress_history`](robot.md#compress_history)
|
|
122
210
|
- [Building Robots](../../guides/building-robots.md) — Robot construction patterns
|
|
123
211
|
- [Structured Delegation](../../guides/building-robots.md#structured-delegation) — `DelegationFuture` and async fan-out
|