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
|
@@ -20,12 +20,20 @@ puts result.last_text_content
|
|
|
20
20
|
|
|
21
21
|
### Name
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
An identifier used for routing, logging, and as the key under which a robot's result is stored in a network's `result.context`. If omitted it defaults to the literal string `"robot"` — nothing is auto-generated, so two unnamed robots share the same name.
|
|
24
24
|
|
|
25
25
|
```ruby
|
|
26
26
|
robot = RobotLab.build(name: "support_agent", system_prompt: "...")
|
|
27
|
+
|
|
28
|
+
RobotLab.build.name # => "robot"
|
|
27
29
|
```
|
|
28
30
|
|
|
31
|
+
> [!NOTE]
|
|
32
|
+
> The default is load-bearing. A robot records whether `name:` was supplied by
|
|
33
|
+
> comparing it against `"robot"`; front-matter `robot_name:` is applied **only**
|
|
34
|
+
> when the constructor left the name at its default. Passing `name: "robot"`
|
|
35
|
+
> explicitly therefore still counts as "not named", and front matter wins.
|
|
36
|
+
|
|
29
37
|
### Description
|
|
30
38
|
|
|
31
39
|
Describes what the robot does (useful for routing decisions):
|
|
@@ -123,16 +131,31 @@ The following YAML front matter keys are applied to the robot's chat automatical
|
|
|
123
131
|
|
|
124
132
|
**LLM Configuration:**
|
|
125
133
|
|
|
126
|
-
| Key | Description |
|
|
127
|
-
|
|
128
|
-
| `model` | Override the LLM model |
|
|
129
|
-
| `temperature` | Controls randomness (0.0 - 1.0) |
|
|
130
|
-
| `top_p` | Nucleus sampling threshold |
|
|
131
|
-
| `top_k` | Top-k sampling |
|
|
132
|
-
| `max_tokens` | Maximum tokens in response |
|
|
133
|
-
| `presence_penalty` | Penalize based on presence |
|
|
134
|
-
| `frequency_penalty` | Penalize based on frequency |
|
|
135
|
-
| `stop` | Stop sequences |
|
|
134
|
+
| Key | Description | Applied from front matter? |
|
|
135
|
+
|-----|-------------|----------------------------|
|
|
136
|
+
| `model` | Override the LLM model | Yes |
|
|
137
|
+
| `temperature` | Controls randomness (0.0 - 1.0) | Yes |
|
|
138
|
+
| `top_p` | Nucleus sampling threshold | **No — silently dropped** |
|
|
139
|
+
| `top_k` | Top-k sampling | **No — silently dropped** |
|
|
140
|
+
| `max_tokens` | Maximum tokens in response | **No — silently dropped** |
|
|
141
|
+
| `presence_penalty` | Penalize based on presence | **No — silently dropped** |
|
|
142
|
+
| `frequency_penalty` | Penalize based on frequency | **No — silently dropped** |
|
|
143
|
+
| `stop` | Stop sequences | **No — silently dropped** |
|
|
144
|
+
|
|
145
|
+
> [!WARNING]
|
|
146
|
+
> Only `model` and `temperature` take effect from front matter. The other six are
|
|
147
|
+
> parsed into the robot's `RunConfig` and then dropped: `RunConfig#apply_to`
|
|
148
|
+
> dispatches `chat.with_<field>` guarded by `respond_to?`, and `RubyLLM::Chat`
|
|
149
|
+
> only implements `with_model` and `with_temperature`. A template declaring all
|
|
150
|
+
> eight leaves the chat's params hash empty — no warning, no error.
|
|
151
|
+
>
|
|
152
|
+
> The same six **do** work as constructor kwargs or via a `config:` `RunConfig`,
|
|
153
|
+
> which route through `with_params`:
|
|
154
|
+
>
|
|
155
|
+
> ```ruby
|
|
156
|
+
> RobotLab.build(name: "w", system_prompt: "...", top_p: 0.5, max_tokens: 1200)
|
|
157
|
+
> # chat params => {top_p: 0.5, max_tokens: 1200}
|
|
158
|
+
> ```
|
|
136
159
|
|
|
137
160
|
**Robot Identity and Capabilities:**
|
|
138
161
|
|
|
@@ -161,15 +184,16 @@ Unlike `template:` on `build` (which renders a template as a robot's *system pro
|
|
|
161
184
|
|
|
162
185
|
Templates can declare everything a robot needs — identity, tools, MCP servers, and LLM config — making the `.md` file a complete robot definition:
|
|
163
186
|
|
|
164
|
-
```markdown title="prompts/
|
|
187
|
+
```markdown title="prompts/my_github_assistant.md"
|
|
165
188
|
---
|
|
166
189
|
description: GitHub assistant with MCP tool access
|
|
167
190
|
robot_name: github_bot
|
|
168
191
|
mcp:
|
|
169
192
|
- name: github
|
|
170
|
-
transport:
|
|
171
|
-
|
|
172
|
-
|
|
193
|
+
transport:
|
|
194
|
+
type: stdio
|
|
195
|
+
command: npx
|
|
196
|
+
args: ["-y", "@modelcontextprotocol/server-github"]
|
|
173
197
|
model: claude-sonnet-4
|
|
174
198
|
temperature: 0.3
|
|
175
199
|
---
|
|
@@ -181,9 +205,22 @@ Build the robot with minimal constructor arguments:
|
|
|
181
205
|
|
|
182
206
|
```ruby
|
|
183
207
|
# Template provides name, description, MCP config, model, and temperature
|
|
184
|
-
robot = RobotLab.build(template: :
|
|
208
|
+
robot = RobotLab.build(template: :my_github_assistant)
|
|
209
|
+
|
|
210
|
+
# MCP still has to be requested at run time — see the warning below
|
|
211
|
+
robot.run("What are the open issues?", mcp: :inherit, tools: :inherit)
|
|
185
212
|
```
|
|
186
213
|
|
|
214
|
+
> [!WARNING]
|
|
215
|
+
> `transport:` **must be a nested hash**. The shipped
|
|
216
|
+
> `examples/prompts/github_assistant.md` uses the flat form
|
|
217
|
+
> (`transport: stdio` with sibling `command:`/`args:` keys), which raises
|
|
218
|
+
> `NoMethodError: undefined method 'transform_keys' for an instance of String`
|
|
219
|
+
> internally. The error is swallowed and logged as a warning, the server lands in
|
|
220
|
+
> `robot.failed_mcp_server_names`, and the robot builds with **zero tools**. That
|
|
221
|
+
> shipped template also declares no `model:` or `temperature:`, so it does not
|
|
222
|
+
> demonstrate the full self-contained pattern shown here.
|
|
223
|
+
|
|
187
224
|
### Tools in Front Matter
|
|
188
225
|
|
|
189
226
|
Declare tool classes by name in the `tools:` key. RobotLab resolves each string to a Ruby constant and instantiates it:
|
|
@@ -201,6 +238,9 @@ You help customers with order inquiries and refunds.
|
|
|
201
238
|
```ruby
|
|
202
239
|
# Tools are loaded from frontmatter — no local_tools: needed
|
|
203
240
|
robot = RobotLab.build(template: :order_support)
|
|
241
|
+
|
|
242
|
+
# ...but they are only sent to the model when the run asks for them
|
|
243
|
+
robot.run("Where is order 12345?", tools: :inherit)
|
|
204
244
|
```
|
|
205
245
|
|
|
206
246
|
Tool classes must be defined and loaded before the robot is built. If a tool name cannot be resolved, it is skipped with a warning.
|
|
@@ -224,19 +264,29 @@ Declare MCP server configurations directly in the template:
|
|
|
224
264
|
description: Developer assistant with filesystem access
|
|
225
265
|
mcp:
|
|
226
266
|
- name: filesystem
|
|
227
|
-
transport:
|
|
228
|
-
|
|
229
|
-
|
|
267
|
+
transport:
|
|
268
|
+
type: stdio
|
|
269
|
+
command: mcp-server-filesystem
|
|
270
|
+
args: ["--root", "/home/user/projects"]
|
|
230
271
|
---
|
|
231
272
|
You are a developer assistant with filesystem access.
|
|
232
273
|
```
|
|
233
274
|
|
|
234
275
|
```ruby
|
|
235
276
|
robot = RobotLab.build(template: :developer)
|
|
277
|
+
robot.run("List the files in lib/", mcp: :inherit, tools: :inherit)
|
|
236
278
|
```
|
|
237
279
|
|
|
238
280
|
Constructor `mcp:` overrides frontmatter `mcp:` when provided.
|
|
239
281
|
|
|
282
|
+
> [!WARNING]
|
|
283
|
+
> `transport:` takes a nested hash — `type:` plus the transport's own keys. A flat
|
|
284
|
+
> `transport: stdio` with sibling `command:`/`args:` keys fails silently (the
|
|
285
|
+
> `transform_keys` NoMethodError is swallowed) and the robot ends up with no MCP
|
|
286
|
+
> tools. Valid `type:` values are `stdio`, `sse`, `ws`, `websocket`,
|
|
287
|
+
> `streamable-http`, and `http`; the underscored `streamable_http` raises
|
|
288
|
+
> `ArgumentError`.
|
|
289
|
+
|
|
240
290
|
### Template with System Prompt
|
|
241
291
|
|
|
242
292
|
You can combine a template and an inline system prompt. Both are applied to the chat -- the template first, then the system prompt is appended as additional instructions:
|
|
@@ -363,7 +413,6 @@ Skills can include LLM configuration in their front matter. Config cascades in p
|
|
|
363
413
|
---
|
|
364
414
|
description: Enable creative responses
|
|
365
415
|
temperature: 0.9
|
|
366
|
-
top_p: 0.95
|
|
367
416
|
---
|
|
368
417
|
Be creative and imaginative in your responses.
|
|
369
418
|
```
|
|
@@ -377,6 +426,12 @@ robot = RobotLab.build(
|
|
|
377
426
|
# temperature is 0.9 from the skill (unless the main template or constructor overrides it)
|
|
378
427
|
```
|
|
379
428
|
|
|
429
|
+
> [!NOTE]
|
|
430
|
+
> Skill front matter is subject to the same limitation as template front matter:
|
|
431
|
+
> only `model` and `temperature` reach the chat. Adding `top_p: 0.95` to
|
|
432
|
+
> `creative_mode.md` would be parsed and then silently discarded. Set it as a
|
|
433
|
+
> constructor kwarg (`top_p: 0.95`) instead.
|
|
434
|
+
|
|
380
435
|
The precedence order (highest wins):
|
|
381
436
|
|
|
382
437
|
1. Constructor kwargs (`temperature: 0.3`)
|
|
@@ -427,8 +482,41 @@ robot = RobotLab.build(
|
|
|
427
482
|
system_prompt: "You help customers with orders.",
|
|
428
483
|
local_tools: [OrderLookup, InventoryCheck]
|
|
429
484
|
)
|
|
485
|
+
|
|
486
|
+
result = robot.run("Where is order 12345?", tools: :inherit)
|
|
430
487
|
```
|
|
431
488
|
|
|
489
|
+
> [!WARNING]
|
|
490
|
+
> **`run` defaults to `tools: :none` and `mcp: :none`.** Attaching tools at build
|
|
491
|
+
> time is not enough — a plain `robot.run("...")` sends the model **zero** tools,
|
|
492
|
+
> because an explicit `:none` means "send no tools this turn" rather than "fall
|
|
493
|
+
> back to the attached set". Pass `tools: :inherit` on the call to send the
|
|
494
|
+
> attached tools, and `mcp: :inherit, tools: :inherit` to connect MCP servers and
|
|
495
|
+
> send their tools.
|
|
496
|
+
>
|
|
497
|
+
> For a **standalone** robot, do not pass `tools: :inherit` at *build* time: the
|
|
498
|
+
> parent level is the global config's `:none`, so it resolves to an allowlist of
|
|
499
|
+
> `["none"]`, which matches nothing. Leave `tools:` unset in the constructor.
|
|
500
|
+
>
|
|
501
|
+
> | build `tools:` | run `tools:` | tools sent |
|
|
502
|
+
> |---|---|---|
|
|
503
|
+
> | unset | `:none` (default) | none |
|
|
504
|
+
> | unset | `:inherit` | all attached — **the correct pattern** |
|
|
505
|
+
> | `:inherit` | `:inherit` | none — the standalone trap |
|
|
506
|
+
> | `:none` | `:inherit` | all attached |
|
|
507
|
+
>
|
|
508
|
+
> This table is for a robot run on its own. Inside a **network** whose `config:`
|
|
509
|
+
> sets `tools:`/`mcp:`, build-time `:inherit` is not a trap — it is exactly how a
|
|
510
|
+
> robot opts in to the network-level list, and the parent is that list rather
|
|
511
|
+
> than `:none`. See
|
|
512
|
+
> [Network-Wide Tool and MCP Defaults](creating-networks.md#network-wide-tool-and-mcp-defaults).
|
|
513
|
+
>
|
|
514
|
+
> An explicit array (`tools: [OrderLookup]`) is an **allowlist** over the attached
|
|
515
|
+
> tools; it selects from them and cannot add new ones. Entries must match how the
|
|
516
|
+
> tool was attached — a tool attached as a class matches its class name
|
|
517
|
+
> (`[OrderLookup]`), one attached as an instance matches RubyLLM's derived name
|
|
518
|
+
> (`%w[order_lookup]`). The two forms do not cross-match.
|
|
519
|
+
|
|
432
520
|
See the [Using Tools](using-tools.md) guide for details on defining tools.
|
|
433
521
|
|
|
434
522
|
## MCP Configuration
|
|
@@ -456,6 +544,14 @@ MCP configuration supports hierarchical resolution:
|
|
|
456
544
|
| `:inherit` | Use parent network/config MCP servers |
|
|
457
545
|
| `[...]` | Explicit array of server configurations |
|
|
458
546
|
|
|
547
|
+
`run` also defaults to `mcp: :none`, so the servers configured above are not connected by a plain `run`:
|
|
548
|
+
|
|
549
|
+
```ruby
|
|
550
|
+
robot.run("Read config/database.yml", mcp: :inherit, tools: :inherit)
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
`robot.connect_mcp!` connects eagerly if you want the handshake to happen up front, but a later plain `run()` still sends no tools — `tools: :inherit` is what puts the MCP tools in the request. Connection failures are logged and recorded in `robot.failed_mcp_server_names`; they are never raised.
|
|
554
|
+
|
|
459
555
|
See the [MCP Integration](mcp-integration.md) guide for transport types and advanced patterns.
|
|
460
556
|
|
|
461
557
|
## Chaining Configuration
|
|
@@ -474,24 +570,35 @@ result = robot
|
|
|
474
570
|
|
|
475
571
|
### Available Chain Methods
|
|
476
572
|
|
|
573
|
+
This is the complete set — the LLM-facing methods are delegated dynamically from `RubyLLM::Chat`, and `with_template` / `with_bus` are RobotLab's own:
|
|
574
|
+
|
|
477
575
|
| Method | Description |
|
|
478
576
|
|--------|-------------|
|
|
479
577
|
| `with_model(id)` | Change the LLM model |
|
|
480
578
|
| `with_instructions(text)` | Set system instructions |
|
|
481
579
|
| `with_temperature(val)` | Set temperature |
|
|
482
|
-
| `
|
|
483
|
-
| `
|
|
484
|
-
| `
|
|
485
|
-
| `with_presence_penalty(val)` | Set presence penalty |
|
|
486
|
-
| `with_frequency_penalty(val)` | Set frequency penalty |
|
|
487
|
-
| `with_stop(sequences)` | Set stop sequences |
|
|
580
|
+
| `with_params(**params)` | Set arbitrary provider params (`top_p`, `max_tokens`, …) |
|
|
581
|
+
| `with_context(ctx)` | Set the RubyLLM context |
|
|
582
|
+
| `with_headers(**headers)` | Set extra request headers |
|
|
488
583
|
| `with_tool(tool)` | Add a single tool |
|
|
489
584
|
| `with_tools(*tools)` | Add multiple tools |
|
|
490
|
-
| `with_template(id, **ctx)` | Apply a prompt template |
|
|
491
585
|
| `with_schema(schema)` | Set structured output schema |
|
|
492
586
|
| `with_thinking(config)` | Enable extended thinking |
|
|
587
|
+
| `with_template(id, **ctx)` | Apply a prompt template |
|
|
493
588
|
| `with_bus(bus)` | Connect to a message bus (creates one if nil) |
|
|
494
589
|
|
|
590
|
+
> [!WARNING]
|
|
591
|
+
> `with_top_p`, `with_top_k`, `with_max_tokens`, `with_presence_penalty`,
|
|
592
|
+
> `with_frequency_penalty`, and `with_stop` **do not exist** — calling any of them
|
|
593
|
+
> raises `NoMethodError`. `RubyLLM::Chat` exposes those knobs through
|
|
594
|
+
> `with_params`, so use either the constructor kwarg or `with_params`:
|
|
595
|
+
>
|
|
596
|
+
> ```ruby
|
|
597
|
+
> robot.with_params(max_tokens: 2000, top_p: 0.3).run("...")
|
|
598
|
+
> # or
|
|
599
|
+
> RobotLab.build(name: "bot", system_prompt: "...", max_tokens: 2000, top_p: 0.3)
|
|
600
|
+
> ```
|
|
601
|
+
|
|
495
602
|
## Running Robots
|
|
496
603
|
|
|
497
604
|
### Standalone
|
|
@@ -508,14 +615,36 @@ The `run` method returns a `RobotResult` with:
|
|
|
508
615
|
```ruby
|
|
509
616
|
result.last_text_content # => "Hi there! How can I help?"
|
|
510
617
|
result.reply # => alias for last_text_content
|
|
511
|
-
result.output # =>
|
|
512
|
-
result.tool_calls # => Array of tool call results
|
|
618
|
+
result.output # => [TextMessage] built from the final response text
|
|
619
|
+
result.tool_calls # => Array of tool call results (see note)
|
|
513
620
|
result.robot_name # => "assistant"
|
|
514
|
-
result.stop_reason # =>
|
|
621
|
+
result.stop_reason # => always nil (see note)
|
|
622
|
+
result.input_tokens # => Integer
|
|
623
|
+
result.output_tokens # => Integer
|
|
515
624
|
result.duration # => Float (elapsed seconds, set in pipeline execution)
|
|
516
625
|
result.raw # => raw LLM response object
|
|
517
626
|
```
|
|
518
627
|
|
|
628
|
+
> [!NOTE]
|
|
629
|
+
> `result.tool_calls` is effectively always empty. It is read from the *final*
|
|
630
|
+
> assistant message, and by the time ruby_llm's tool loop has finished that
|
|
631
|
+
> message carries no tool calls. Use `:tool_call` [hooks](hooks.md) or the
|
|
632
|
+
> `on_tool_call:` / `on_tool_result:` callbacks to observe tool activity.
|
|
633
|
+
> Similarly, `result.output` holds only the final response text, not the full
|
|
634
|
+
> turn. There is no `result.content` and no `result.text?`.
|
|
635
|
+
|
|
636
|
+
> [!WARNING]
|
|
637
|
+
> `result.stop_reason` is **always `nil`**. `RubyLLM::Message` does not define
|
|
638
|
+
> `stop_reason`, and `build_result` fills the field with
|
|
639
|
+
> `response.respond_to?(:stop_reason) ? response.stop_reason : nil` — so no
|
|
640
|
+
> provider value ever lands there, and `.compact` drops the key from
|
|
641
|
+
> `result.export` entirely. Do not branch on `"end_turn"`, `"tool_use"`, or
|
|
642
|
+
> `"stop"`. Consequently `result.stopped?` is simply "this result has no tool
|
|
643
|
+
> calls".
|
|
644
|
+
>
|
|
645
|
+
> (`RobotLab::Message::VALID_STOP_REASONS` is `["tool", "stop"]`, but that
|
|
646
|
+
> constant governs the `Message` classes you build yourself, not `RobotResult`.)
|
|
647
|
+
|
|
519
648
|
### With Runtime Memory
|
|
520
649
|
|
|
521
650
|
Inject memory values for a single run:
|
|
@@ -569,7 +698,7 @@ robot = RobotLab.build(
|
|
|
569
698
|
robot.run("Tell me a story") { |chunk| stream_to_client(chunk.content) }
|
|
570
699
|
```
|
|
571
700
|
|
|
572
|
-
|
|
701
|
+
`on_content` is also a `RunConfig` field, so it can be supplied through a `config:` on the robot itself rather than as a constructor kwarg:
|
|
573
702
|
|
|
574
703
|
```ruby
|
|
575
704
|
config = RobotLab::RunConfig.new(
|
|
@@ -578,6 +707,13 @@ config = RobotLab::RunConfig.new(
|
|
|
578
707
|
robot = RobotLab.build(name: "bot", system_prompt: "...", config: config)
|
|
579
708
|
```
|
|
580
709
|
|
|
710
|
+
> [!WARNING]
|
|
711
|
+
> This only works for the robot's **own** config. A network-level `config:` does
|
|
712
|
+
> **not** supply `on_content` (or any other callback or LLM field) to its member
|
|
713
|
+
> robots — a network propagates only `mcp` and `tools`. Each robot reads
|
|
714
|
+
> `on_content` from its own config at construction time, so streaming callbacks
|
|
715
|
+
> must be set per robot.
|
|
716
|
+
|
|
581
717
|
You can also monitor tool activity via callbacks:
|
|
582
718
|
|
|
583
719
|
```ruby
|
|
@@ -640,8 +776,12 @@ billing_specialist = RobotLab.build(
|
|
|
640
776
|
context: { department: "billing" },
|
|
641
777
|
local_tools: [InvoiceLookup, RefundProcessor]
|
|
642
778
|
)
|
|
779
|
+
|
|
780
|
+
billing_specialist.run("Refund order 12345", tools: :inherit)
|
|
643
781
|
```
|
|
644
782
|
|
|
783
|
+
In a network, the equivalent opt-in is on the task: `task :billing, billing_specialist, tools: :inherit, depends_on: :optional`.
|
|
784
|
+
|
|
645
785
|
### Summarizer Robot
|
|
646
786
|
|
|
647
787
|
Condense information:
|
|
@@ -710,7 +850,16 @@ bob.serve # every inbound task runs through bob.run and replies automatically
|
|
|
710
850
|
alice.send_message(to: :bob, content: "Tell me a funny robot joke.")
|
|
711
851
|
```
|
|
712
852
|
|
|
713
|
-
`serve` is shorthand for `respond_to_tasks(auto_reply: true) { |message| run(message
|
|
853
|
+
`serve` is shorthand for `respond_to_tasks(auto_reply: true) { |message| run(bus_task_content(message)).reply }`.
|
|
854
|
+
|
|
855
|
+
`bus_task_content` flattens the message payload for `run`: a `String` content is passed through via `to_s`, while a `Hash` content becomes one `"key: value"` line per entry. So a task sent as `{ topic: "robots", style: "dry" }` reaches the LLM as:
|
|
856
|
+
|
|
857
|
+
```
|
|
858
|
+
topic: robots
|
|
859
|
+
style: dry
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
Use `respond_to_tasks` directly when the reply shouldn't just be `run(...).reply` — e.g. to build the prompt yourself, or to post-process the result:
|
|
714
863
|
|
|
715
864
|
```ruby
|
|
716
865
|
bob.respond_to_tasks do |message|
|
|
@@ -776,7 +925,7 @@ bot.with_bus(existing_bus) # now connected and can send/receive messages
|
|
|
776
925
|
|
|
777
926
|
## Context Window Compression
|
|
778
927
|
|
|
779
|
-
Long-running robots accumulate conversation history that can grow to fill the context window. `compress_history` prunes old turns using
|
|
928
|
+
Long-running robots accumulate conversation history that can grow to fill the context window. `compress_history` prunes old turns using stemmed term-frequency cosine similarity (term frequencies only — no IDF weighting) against the most recent context, keeping turns that are still relevant and discarding or summarizing those that aren't.
|
|
780
929
|
|
|
781
930
|
```ruby
|
|
782
931
|
# Default settings: protect 3 most-recent turn pairs, drop anything below 0.2
|
|
@@ -813,7 +962,7 @@ gem "classifier", "~> 2.3"
|
|
|
813
962
|
|
|
814
963
|
## Convergence Detection
|
|
815
964
|
|
|
816
|
-
`RobotLab::Convergence` uses
|
|
965
|
+
`RobotLab::Convergence` uses stemmed term-frequency cosine similarity (not TF-IDF — on a two-document corpus, IDF suppresses exactly the shared terms that signal agreement) to detect when two independent agents have reached the same conclusion. The primary use case is skipping an expensive reconciler robot when two verifiers already agree. Texts shorter than 30 characters always score `0.0`.
|
|
817
966
|
|
|
818
967
|
```ruby
|
|
819
968
|
# Check the similarity score directly (returns Float 0.0..1.0)
|
|
@@ -826,26 +975,41 @@ RobotLab::Convergence.detected?(result_a.reply, result_b.reply)
|
|
|
826
975
|
RobotLab::Convergence.detected?(text_a, text_b, threshold: 0.75)
|
|
827
976
|
```
|
|
828
977
|
|
|
829
|
-
Wire it into a network
|
|
978
|
+
Wire it into a network for the reconciler fast-path. There is no router object in RobotLab — the reconciler is declared `depends_on: :optional` and a gate robot activates it only when the verifiers disagree:
|
|
830
979
|
|
|
831
980
|
```ruby
|
|
832
981
|
verifier_a = RobotLab.build(name: "verifier_a", system_prompt: "Verify the answer.")
|
|
833
982
|
verifier_b = RobotLab.build(name: "verifier_b", system_prompt: "Independently verify the answer.")
|
|
834
983
|
reconciler = RobotLab.build(name: "reconciler", system_prompt: "Reconcile conflicting answers.")
|
|
835
984
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
985
|
+
class ConvergenceGate < RobotLab::Robot
|
|
986
|
+
def call(result)
|
|
987
|
+
a = result.context[:verifier_a]&.reply.to_s # keyed by ROBOT name
|
|
988
|
+
b = result.context[:verifier_b]&.reply.to_s
|
|
989
|
+
|
|
990
|
+
return result if RobotLab::Convergence.detected?(a, b) # agree — skip reconciler
|
|
839
991
|
|
|
840
|
-
|
|
841
|
-
|
|
992
|
+
result.activate(:reconciler)
|
|
993
|
+
end
|
|
842
994
|
end
|
|
843
995
|
|
|
844
|
-
network = RobotLab.create_network(name: "verify"
|
|
845
|
-
|
|
996
|
+
network = RobotLab.create_network(name: "verify") do
|
|
997
|
+
task :verifier_a, verifier_a, depends_on: :none
|
|
998
|
+
task :verifier_b, verifier_b, depends_on: :none
|
|
999
|
+
task :gate, ConvergenceGate.new(name: "gate"), depends_on: %i[verifier_a verifier_b]
|
|
1000
|
+
task :reconciler, reconciler, depends_on: :optional
|
|
846
1001
|
end
|
|
1002
|
+
|
|
1003
|
+
network.run(message: "Is the deployment healthy?").activated_steps
|
|
1004
|
+
# => [] when they agreed, [:reconciler] when they diverged
|
|
847
1005
|
```
|
|
848
1006
|
|
|
1007
|
+
> [!WARNING]
|
|
1008
|
+
> `RobotLab.create_network` accepts only `name:`, `concurrency:`, `config:`, and a
|
|
1009
|
+
> block. There are no `router:` or `robots:` keyword arguments — passing them
|
|
1010
|
+
> raises `ArgumentError: unknown keywords: :robots, :router` — and no `Router` or
|
|
1011
|
+
> `Router::Args` class exists anywhere in the library.
|
|
1012
|
+
|
|
849
1013
|
Requires the `classifier` gem (`~> 2.3`).
|
|
850
1014
|
|
|
851
1015
|
## Structured Delegation
|
|
@@ -901,20 +1065,64 @@ result = f1.value(timeout: 30)
|
|
|
901
1065
|
|
|
902
1066
|
## Configuration
|
|
903
1067
|
|
|
904
|
-
RobotLab uses `MywayConfig` for configuration.
|
|
1068
|
+
RobotLab uses `MywayConfig` for configuration. Read values off the config object directly:
|
|
905
1069
|
|
|
906
1070
|
```ruby
|
|
907
|
-
RobotLab.config.ruby_llm.model
|
|
1071
|
+
RobotLab.config.ruby_llm.model # => "claude-sonnet-4"
|
|
908
1072
|
RobotLab.config.ruby_llm.request_timeout # => 120
|
|
909
1073
|
```
|
|
910
1074
|
|
|
911
|
-
|
|
1075
|
+
`RobotLab.configure` also exists, and yields the config object for imperative setup:
|
|
1076
|
+
|
|
1077
|
+
```ruby
|
|
1078
|
+
RobotLab.configure do |config|
|
|
1079
|
+
config.logger = Logger.new($stdout)
|
|
1080
|
+
end
|
|
1081
|
+
```
|
|
912
1082
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
-
|
|
917
|
-
|
|
1083
|
+
Configuration is layered, lowest precedence first:
|
|
1084
|
+
|
|
1085
|
+
1. Bundled defaults (`lib/robot_lab/config/defaults.yml`)
|
|
1086
|
+
2. Environment-specific overrides (development, test, production)
|
|
1087
|
+
3. XDG user config (`~/.config/robot_lab/robot_lab.yml`)
|
|
1088
|
+
4. Project config (`./config/robot_lab.yml`)
|
|
1089
|
+
5. Environment variables (`ROBOT_LAB_*` prefix; `__` for nesting)
|
|
1090
|
+
6. Constructor parameters
|
|
1091
|
+
|
|
1092
|
+
> [!WARNING]
|
|
1093
|
+
> The XDG file is `~/.config/robot_lab/**robot_lab.yml**` — the filename repeats
|
|
1094
|
+
> the app name. `~/.config/robot_lab/config.yml` is never read.
|
|
1095
|
+
>
|
|
1096
|
+
> A top-level `defaults:` wrapper is **always** ignored — that key means
|
|
1097
|
+
> something only inside the gem's own bundled `defaults.yml`. Write
|
|
1098
|
+
> `max_tool_rounds: 12`, not `defaults:\n max_tool_rounds: 12`.
|
|
1099
|
+
>
|
|
1100
|
+
> An **environment-named** wrapper is a different story, and the two config
|
|
1101
|
+
> files behave differently:
|
|
1102
|
+
>
|
|
1103
|
+
> | File | flat keys | `development:` / `test:` / `production:` wrapper |
|
|
1104
|
+
> |---|---|---|
|
|
1105
|
+
> | `~/.config/robot_lab/robot_lab.yml` | honored | **honored** for the current environment |
|
|
1106
|
+
> | `./config/robot_lab.yml` (no Rails) | honored | ignored |
|
|
1107
|
+
> | `./config/robot_lab.yml` (in Rails) | ignored | **required** — keys must be nested under `Rails.env` |
|
|
1108
|
+
>
|
|
1109
|
+
> The XDG loader checks for a section named for the current environment and only
|
|
1110
|
+
> falls back to the file root when there is none. Outside Rails the environment
|
|
1111
|
+
> defaults to `development` (or `RAILS_ENV` / `RACK_ENV` when set), so a
|
|
1112
|
+
> `development:` section in the XDG file takes effect while `test:` and
|
|
1113
|
+
> `production:` sections sit dormant. Verified with a `max_iterations: 777`
|
|
1114
|
+
> XDG file: flat → 777, `development:` → 777, `production:` → 10 (until
|
|
1115
|
+
> `RACK_ENV=production`, then 777), `defaults:` → 10.
|
|
1116
|
+
>
|
|
1117
|
+
> Inside Rails, `anyway_config` sets the current environment to `Rails.env`,
|
|
1118
|
+
> which makes the **project** file environmental too — a flat
|
|
1119
|
+
> `./config/robot_lab.yml` is then ignored.
|
|
1120
|
+
>
|
|
1121
|
+
> ERB is evaluated only in `./config/robot_lab.yml`. The XDG loader uses
|
|
1122
|
+
> `YAML.safe_load` with no ERB pass, so `<%= ENV['KEY'] %>` there stays a literal
|
|
1123
|
+
> string. Nested env vars also arrive as strings
|
|
1124
|
+
> (`ROBOT_LAB_RUBY_LLM__REQUEST_TIMEOUT=180` yields `"180"`); top-level keys are
|
|
1125
|
+
> type-coerced.
|
|
918
1126
|
|
|
919
1127
|
## Best Practices
|
|
920
1128
|
|