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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 36b770d963e004a951cdf35d07a2e867e4e661a9e342e8047184cfd9dafd6e15
|
|
4
|
+
data.tar.gz: f53bb5c52efcd6169bf36afc69306a9c03e9cfcabdaf5b0a94871802e2909f55
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 850bde0e9ac1b7d65176bd067f42751d6d9db64517ca665d4765837fc5493e204f9b333cac08019cb451962d4d95789f986dee2ff08e55ae5e6a0d3d034f4f50
|
|
7
|
+
data.tar.gz: 97eb4dc66458a9a43f861e3f9c927eb6cfa63d33ddfff150b4181a45dc2c0db9570095f2e71d1182b76b4ee85e7086a6014b5536e805d9956f0860f9b4a03f50
|
data/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# RobotLab
|
|
2
2
|
|
|
3
|
-
> [!
|
|
4
|
-
> See the [CHANGELOG](CHANGELOG.md) for the latest changes. The [examples directory has a good cross section
|
|
3
|
+
> [!NOTE]
|
|
4
|
+
> See the [CHANGELOG](CHANGELOG.md) for the latest changes. The [examples directory has a good cross section of demo apps](examples/README.md) that show-off the various capabilities of the RobotLab library.
|
|
5
5
|
|
|
6
6
|
<br>
|
|
7
7
|
<table>
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
- <strong>Human-in-the-Loop</strong> - AskUser tool for interactive prompting<br>
|
|
22
22
|
- <strong>Content Streaming</strong> - Stored callbacks, per-call blocks, or both<br>
|
|
23
23
|
- <strong>MCP Integration</strong> - Connect to external tool servers with timeouts and retry<br>
|
|
24
|
-
- <strong>Local LLM Providers</strong> - Ollama
|
|
24
|
+
- <strong>Local LLM Providers</strong> - Ollama and GPUStack via provider passthrough<br>
|
|
25
25
|
- <strong>Shared Memory</strong> - Reactive key-value store with subscriptions<br>
|
|
26
26
|
- <strong>Message Bus</strong> - Bidirectional robot communication via TypedBus<br>
|
|
27
27
|
- <strong>Dynamic Spawning</strong> - Robots create new robots at runtime<br>
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
- <strong>Rails Integration</strong> - Generators, background jobs, Turbo Stream broadcasting (via <a href="https://github.com/MadBomber/robot_lab-rails">robot_lab-rails</a>)<br>
|
|
31
31
|
- <strong>Token & Cost Tracking</strong> - Per-run and cumulative token counts on every robot<br>
|
|
32
32
|
- <strong>Tool Loop Circuit Breaker</strong> - <code>max_tool_rounds:</code> guards against runaway tool call loops<br>
|
|
33
|
+
- <strong>Doom Loop Detection</strong> - always-on detection of repeated or cyclic tool call patterns, tunable via <code>doom_loop_threshold:</code><br>
|
|
33
34
|
- <strong>Learning Accumulation</strong> - <code>robot.learn()</code> builds up cross-run observations with deduplication<br>
|
|
34
35
|
- <strong>Context Window Compression</strong> - <code>robot.compress_history()</code> prunes irrelevant old turns via TF cosine scoring<br>
|
|
35
36
|
- <strong>Convergence Detection</strong> - <code>RobotLab::Convergence</code> detects when independent agents agree, enabling reconciler fast-path<br>
|
|
@@ -38,7 +39,7 @@
|
|
|
38
39
|
</tr>
|
|
39
40
|
</table>
|
|
40
41
|
|
|
41
|
-
<p>RobotLab enables sophisticated AI applications using multiple specialized robots (LLM agents) that work together to accomplish complex tasks. Each robot has its own instructions, skills, tools, and capabilities. Review the [full documentation website](https://madbomber.github.io/robot_lab)
|
|
42
|
+
<p>RobotLab enables sophisticated AI applications using multiple specialized robots (LLM agents) that work together to accomplish complex tasks. Each robot has its own instructions, skills, tools, and capabilities. Review the [full documentation website](https://madbomber.github.io/robot_lab) and explore the [many examples](examples/README.md) available as working demo applications.</p>
|
|
42
43
|
|
|
43
44
|
## Installation
|
|
44
45
|
|
|
@@ -81,7 +82,11 @@ puts result.last_text_content
|
|
|
81
82
|
|
|
82
83
|
### Local LLM Providers
|
|
83
84
|
|
|
84
|
-
For local LLM providers (
|
|
85
|
+
For local LLM providers, use the `provider:` parameter. RubyLLM's provider list is the authority on valid values — `:ollama` and `:gpustack` are the local ones (alongside `:anthropic`, `:azure`, `:bedrock`, `:deepseek`, `:gemini`, `:mistral`, `:openai`, `:openrouter`, `:perplexity`, `:vertexai`, and `:xai`). The provider's API base must be configured first, or building the robot raises `RubyLLM::ConfigurationError`:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
export ROBOT_LAB_RUBY_LLM__OLLAMA_API_BASE=http://localhost:11434/v1
|
|
89
|
+
```
|
|
85
90
|
|
|
86
91
|
```ruby
|
|
87
92
|
robot = RobotLab.build(
|
|
@@ -92,13 +97,15 @@ robot = RobotLab.build(
|
|
|
92
97
|
)
|
|
93
98
|
```
|
|
94
99
|
|
|
100
|
+
Specifying `provider:` also sets `assume_model_exists`, so the model name is not validated against a known-model list.
|
|
101
|
+
|
|
95
102
|
### Configuration
|
|
96
103
|
|
|
97
104
|
RobotLab uses [MywayConfig](https://github.com/MadBomber/myway_config) for layered configuration. Configuration is loaded automatically from multiple sources in priority order:
|
|
98
105
|
|
|
99
106
|
1. Bundled defaults (`lib/robot_lab/config/defaults.yml`)
|
|
100
107
|
2. Environment-specific overrides (development, test, production)
|
|
101
|
-
3. XDG user config (`~/.config/robot_lab/
|
|
108
|
+
3. XDG user config (`~/.config/robot_lab/robot_lab.yml` — note the filename repeats the app name)
|
|
102
109
|
4. Project config (`./config/robot_lab.yml`)
|
|
103
110
|
5. Environment variables (`ROBOT_LAB_*` prefix)
|
|
104
111
|
|
|
@@ -113,7 +120,6 @@ export ROBOT_LAB_RUBY_LLM__MODEL=claude-sonnet-4
|
|
|
113
120
|
# Access configuration values
|
|
114
121
|
RobotLab.config.ruby_llm.model #=> "claude-sonnet-4"
|
|
115
122
|
RobotLab.config.ruby_llm.request_timeout #=> 120
|
|
116
|
-
RobotLab.config.streaming_enabled #=> true
|
|
117
123
|
```
|
|
118
124
|
|
|
119
125
|
Or create a project config file at `./config/robot_lab.yml`:
|
|
@@ -192,9 +198,10 @@ tools:
|
|
|
192
198
|
- CodeSearchTool
|
|
193
199
|
mcp:
|
|
194
200
|
- name: github
|
|
195
|
-
transport:
|
|
196
|
-
|
|
197
|
-
|
|
201
|
+
transport:
|
|
202
|
+
type: stdio
|
|
203
|
+
command: npx
|
|
204
|
+
args: ["-y", "@modelcontextprotocol/server-github"]
|
|
198
205
|
model: claude-sonnet-4
|
|
199
206
|
---
|
|
200
207
|
You are a GitHub assistant. Use available tools to help with repository tasks.
|
|
@@ -205,7 +212,10 @@ You are a GitHub assistant. Use available tools to help with repository tasks.
|
|
|
205
212
|
robot = RobotLab.build(template: :github_assistant)
|
|
206
213
|
```
|
|
207
214
|
|
|
208
|
-
Front matter supports: `description`, `robot_name`, `tools`, `mcp`, `skills`, `parameters`, and LLM config keys
|
|
215
|
+
Front matter supports: `description`, `robot_name`, `tools`, `mcp`, `skills`, `parameters`, and LLM config keys. Constructor-provided values always take precedence over front matter.
|
|
216
|
+
|
|
217
|
+
> [!IMPORTANT]
|
|
218
|
+
> Of the LLM config keys, only **`model`** and **`temperature`** currently take effect from front matter. `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, and `stop` are parsed but not applied — set those as constructor kwargs (or via `config:`), where they do work.
|
|
209
219
|
|
|
210
220
|
### Composable Skills
|
|
211
221
|
|
|
@@ -237,7 +247,7 @@ Skills are expanded depth-first and can reference other skills (with automatic c
|
|
|
237
247
|
|
|
238
248
|
### Combining Templates with System Prompts
|
|
239
249
|
|
|
240
|
-
The `system_prompt` parameter can also be used alongside a template. When both are provided, the template renders first and the `system_prompt` is appended. This is particularly useful during development and testing when you want to add temporary instructions or context to an existing template:
|
|
250
|
+
The `system_prompt` parameter can also be used alongside a template. When both are provided, the template renders first and the `system_prompt` is appended, producing a single combined system message. The appended text also survives template re-rendering, so it persists across `run()` calls that supply runtime context. This is particularly useful during development and testing when you want to add temporary instructions or context to an existing template:
|
|
241
251
|
|
|
242
252
|
```ruby
|
|
243
253
|
robot = RobotLab.build(
|
|
@@ -250,7 +260,7 @@ robot = RobotLab.build(
|
|
|
250
260
|
|
|
251
261
|
### Shared Configuration with RunConfig
|
|
252
262
|
|
|
253
|
-
`RunConfig` lets you define operational defaults
|
|
263
|
+
`RunConfig` lets you define operational defaults. For a single robot they cascade from least- to most-specific: template front matter -> `config:` -> constructor kwargs. Front matter is the *base*, not an override — constructor kwargs always win.
|
|
254
264
|
|
|
255
265
|
```ruby
|
|
256
266
|
# Create a shared config
|
|
@@ -267,7 +277,7 @@ robot = RobotLab.build(
|
|
|
267
277
|
config: shared
|
|
268
278
|
)
|
|
269
279
|
|
|
270
|
-
# Apply
|
|
280
|
+
# Apply at the network level (see the note below on what actually propagates)
|
|
271
281
|
network = RobotLab.create_network(name: "pipeline", config: shared) do
|
|
272
282
|
task :analyzer, analyzer_robot, depends_on: :none
|
|
273
283
|
task :writer, writer_robot, depends_on: [:analyzer]
|
|
@@ -282,6 +292,11 @@ robot = RobotLab.build(
|
|
|
282
292
|
)
|
|
283
293
|
```
|
|
284
294
|
|
|
295
|
+
> [!IMPORTANT]
|
|
296
|
+
> A network-level `config:` propagates only `mcp` and `tools` down to its robots, and only when the robot opts in with `mcp: :inherit` / `tools: :inherit` — `:none` at any level resolves to an empty list. LLM fields (`model`, `temperature`, `max_tokens`, …) and callbacks (`on_content`) are read from each robot's own config at construction time, so they are **not** inherited from the network. To share LLM settings, pass the same `RunConfig` to each robot via `config:`.
|
|
297
|
+
>
|
|
298
|
+
> `max_concurrent_robots` is the one field the network itself consumes.
|
|
299
|
+
|
|
285
300
|
RunConfig supports keyword construction, block DSL, and merge semantics:
|
|
286
301
|
|
|
287
302
|
```ruby
|
|
@@ -301,7 +316,7 @@ effective.model #=> "claude-sonnet-4"
|
|
|
301
316
|
|
|
302
317
|
### Chaining Configuration
|
|
303
318
|
|
|
304
|
-
Robots support method chaining to adjust configuration after creation
|
|
319
|
+
Robots support method chaining to adjust configuration after creation. The `with_*` methods are delegated dynamically from the underlying `RubyLLM::Chat`, so the available set is whatever that class defines — currently `with_context`, `with_headers`, `with_instructions`, `with_model`, `with_params`, `with_schema`, `with_temperature`, `with_thinking`, `with_tool`, and `with_tools`. Each returns the robot, so calls chain.
|
|
305
320
|
|
|
306
321
|
```ruby
|
|
307
322
|
robot = RobotLab.build(name: "writer", system_prompt: "You are a creative writer.")
|
|
@@ -309,10 +324,18 @@ robot = RobotLab.build(name: "writer", system_prompt: "You are a creative writer
|
|
|
309
324
|
result = robot
|
|
310
325
|
.with_temperature(0.9)
|
|
311
326
|
.with_model("claude-sonnet-4")
|
|
312
|
-
.with_max_tokens(2000)
|
|
313
327
|
.run("Write a haiku about Ruby programming")
|
|
314
328
|
```
|
|
315
329
|
|
|
330
|
+
There is no `with_max_tokens`. Set token limits in the constructor, or pass them through `with_params`:
|
|
331
|
+
|
|
332
|
+
```ruby
|
|
333
|
+
robot = RobotLab.build(name: "writer", system_prompt: "...", max_tokens: 2000)
|
|
334
|
+
|
|
335
|
+
# or, after creation
|
|
336
|
+
robot.with_params(max_tokens: 2000)
|
|
337
|
+
```
|
|
338
|
+
|
|
316
339
|
## Graceful Tool Error Handling
|
|
317
340
|
|
|
318
341
|
`RobotLab::Tool` automatically catches exceptions in `execute` and returns a plain-text error to the LLM instead of crashing the run. The LLM can then reason about the error and try an alternative approach.
|
|
@@ -368,9 +391,22 @@ robot = RobotLab.build(
|
|
|
368
391
|
local_tools: [Magic8Ball]
|
|
369
392
|
)
|
|
370
393
|
|
|
371
|
-
|
|
394
|
+
# NOTE the `tools: :inherit` — see the warning below
|
|
395
|
+
result = robot.run("Should I start learning Rust?", tools: :inherit)
|
|
372
396
|
```
|
|
373
397
|
|
|
398
|
+
> [!WARNING]
|
|
399
|
+
> `run()` defaults to `tools: :none` and `mcp: :none`, and `:none` means **send zero tools this turn**. Attaching tools with `local_tools:` (or servers with `mcp:`) is therefore not enough on its own — a plain `robot.run("...")` sends the LLM no tools at all and connects no MCP servers.
|
|
400
|
+
>
|
|
401
|
+
> Pass `tools: :inherit` / `mcp: :inherit` on each run to use what the robot has attached:
|
|
402
|
+
>
|
|
403
|
+
> ```ruby
|
|
404
|
+
> robot.run("...", tools: :inherit) # send attached tools
|
|
405
|
+
> robot.run("...", mcp: :inherit, tools: :inherit) # connect MCP + send its tools
|
|
406
|
+
> ```
|
|
407
|
+
>
|
|
408
|
+
> `:inherit` means "no filter, use everything attached"; an explicit array acts as a name allowlist. Note that `tools: :inherit` at *build* time behaves differently — it resolves against the parent level, so keep `:inherit` on the `run()` call.
|
|
409
|
+
|
|
374
410
|
## Orchestrating Multiple Robots
|
|
375
411
|
|
|
376
412
|
Networks use [SimpleFlow](https://github.com/MadBomber/simple_flow) pipelines with optional task activation for intelligent routing:
|
|
@@ -423,24 +459,29 @@ puts result.value.last_text_content
|
|
|
423
459
|
|
|
424
460
|
## Memory
|
|
425
461
|
|
|
426
|
-
|
|
462
|
+
Two independent mechanisms carry state across runs, and it is worth keeping them straight:
|
|
463
|
+
|
|
464
|
+
- **Chat history** — each robot owns a persistent `RubyLLM::Chat`, so conversational context carries forward automatically with no work on your part.
|
|
465
|
+
- **`RobotLab::Memory`** — an explicit key-value store you read and write yourself, shared across robots when they run inside a network.
|
|
427
466
|
|
|
428
467
|
```ruby
|
|
429
|
-
# Standalone robot with inherent memory
|
|
430
468
|
robot = RobotLab.build(name: "assistant", system_prompt: "You are helpful.")
|
|
431
469
|
|
|
432
470
|
robot.run("My name is Alice")
|
|
433
|
-
robot.run("What's my name?") #
|
|
471
|
+
robot.run("What's my name?") # answered from the persistent chat history
|
|
434
472
|
|
|
435
|
-
# Access robot's memory
|
|
473
|
+
# Access robot's memory (the explicit key-value store)
|
|
436
474
|
robot.memory[:user_id] = 123
|
|
437
475
|
robot.memory.data[:category] = "billing"
|
|
438
476
|
|
|
439
477
|
# Runtime memory injection
|
|
440
478
|
robot.run("Help me", memory: { session_id: "abc123", tier: "premium" })
|
|
441
479
|
|
|
442
|
-
# Reset
|
|
480
|
+
# Reset the key-value store (does NOT clear chat history)
|
|
443
481
|
robot.reset_memory
|
|
482
|
+
|
|
483
|
+
# Clear chat history instead, keeping the system prompt
|
|
484
|
+
robot.clear_messages
|
|
444
485
|
```
|
|
445
486
|
|
|
446
487
|
Networks pass context through SimpleFlow::Result:
|
|
@@ -467,6 +508,8 @@ billing_result = result.context[:billing]
|
|
|
467
508
|
puts result.value.last_text_content
|
|
468
509
|
```
|
|
469
510
|
|
|
511
|
+
Results are keyed in `result.context` by the **robot's** `name`, not by the task name given to `task`. Keep the two identical unless you have a reason not to.
|
|
512
|
+
|
|
470
513
|
## MCP Integration
|
|
471
514
|
|
|
472
515
|
Connect to external tool servers via Model Context Protocol:
|
|
@@ -490,17 +533,18 @@ robot = RobotLab.build(
|
|
|
490
533
|
mcp: [filesystem_server]
|
|
491
534
|
)
|
|
492
535
|
|
|
493
|
-
#
|
|
536
|
+
# Connect. Do this explicitly — run() defaults to mcp: :none, so a plain
|
|
537
|
+
# run() never connects the servers configured above.
|
|
494
538
|
robot.connect_mcp!
|
|
495
539
|
|
|
496
540
|
# Check connection status
|
|
497
541
|
puts "Failed: #{robot.failed_mcp_server_names}" if robot.failed_mcp_server_names.any?
|
|
498
542
|
|
|
499
|
-
# Robot can now use filesystem tools
|
|
500
|
-
result = robot.run("List the files in the current directory")
|
|
543
|
+
# Robot can now use filesystem tools — :inherit is required on the run
|
|
544
|
+
result = robot.run("List the files in the current directory", mcp: :inherit, tools: :inherit)
|
|
501
545
|
```
|
|
502
546
|
|
|
503
|
-
MCP connections are resilient: failed servers are automatically retried on subsequent `run()` calls, and one failing server does not prevent others from connecting.
|
|
547
|
+
MCP connections are resilient: failed servers are automatically retried on subsequent `run(mcp: :inherit)` calls, and one failing server does not prevent others from connecting. Connection failures are logged and recorded in `failed_mcp_server_names` rather than raised, so a robot whose servers all failed still builds and runs — just without those tools.
|
|
504
548
|
|
|
505
549
|
## Message Bus
|
|
506
550
|
|
|
@@ -634,7 +678,7 @@ robot = RobotLab.build(
|
|
|
634
678
|
robot.run("Tell me a story") { |chunk| stream_to_client(chunk.content) }
|
|
635
679
|
```
|
|
636
680
|
|
|
637
|
-
The `on_content:` callback
|
|
681
|
+
The `on_content:` callback is a `RunConfig` field, so it can also be supplied through a robot's `config:` instead of as a constructor kwarg. It is read from the robot's own config at construction time — a network-level `config:` does not supply it.
|
|
638
682
|
|
|
639
683
|
## Token & Cost Tracking
|
|
640
684
|
|
|
@@ -675,7 +719,8 @@ robot = RobotLab.build(
|
|
|
675
719
|
begin
|
|
676
720
|
robot.run("Run all steps.")
|
|
677
721
|
rescue RobotLab::ToolLoopError => e
|
|
678
|
-
puts e.message
|
|
722
|
+
puts e.message
|
|
723
|
+
# "Circuit breaker triggered: 11 tool calls exceeded max_tool_rounds (10)"
|
|
679
724
|
end
|
|
680
725
|
```
|
|
681
726
|
|
|
@@ -688,14 +733,16 @@ result = robot.run("Something new.") # robot is healthy again
|
|
|
688
733
|
|
|
689
734
|
## Doom Loop Detection
|
|
690
735
|
|
|
691
|
-
Doom loop detection catches the subtler failure mode where a robot repeats the same tool call pattern indefinitely — not hitting `max_tool_rounds`, but cycling through the same sequence over and over.
|
|
736
|
+
Doom loop detection catches the subtler failure mode where a robot repeats the same tool call pattern indefinitely — not hitting `max_tool_rounds`, but cycling through the same sequence over and over.
|
|
737
|
+
|
|
738
|
+
Unlike the circuit breaker, doom loop detection is **always active** — it is installed on every run. `doom_loop_threshold:` tunes the sensitivity; it does not switch the feature on. The default threshold is `3`.
|
|
692
739
|
|
|
693
740
|
```ruby
|
|
694
741
|
robot = RobotLab.build(
|
|
695
742
|
name: "runner",
|
|
696
743
|
system_prompt: "Execute steps.",
|
|
697
744
|
local_tools: [StepTool],
|
|
698
|
-
doom_loop_threshold:
|
|
745
|
+
doom_loop_threshold: 5 # tolerate more repetition before warning (default: 3)
|
|
699
746
|
)
|
|
700
747
|
```
|
|
701
748
|
|
|
@@ -710,27 +757,33 @@ robot = RobotLab.build(name: "runner", system_prompt: "...", config: config)
|
|
|
710
757
|
|
|
711
758
|
`auto_compact` triggers context window compression automatically before each `run()`, preventing context overflow without manual intervention.
|
|
712
759
|
|
|
760
|
+
`auto_compact` and `compact_threshold` are `RunConfig` fields, not constructor keywords — pass them via `config:`:
|
|
761
|
+
|
|
713
762
|
```ruby
|
|
714
763
|
# Built-in trigger: compact when estimated token usage exceeds 80% of context window
|
|
715
764
|
robot = RobotLab.build(
|
|
716
765
|
name: "analyst",
|
|
717
766
|
system_prompt: "You are a research analyst.",
|
|
718
|
-
auto_compact: :context_window
|
|
767
|
+
config: RobotLab::RunConfig.new(auto_compact: :context_window)
|
|
719
768
|
)
|
|
720
769
|
|
|
721
770
|
# Tune the threshold (here: compact at 70%)
|
|
722
771
|
robot = RobotLab.build(
|
|
723
772
|
name: "analyst",
|
|
724
773
|
system_prompt: "You are a research analyst.",
|
|
725
|
-
|
|
726
|
-
|
|
774
|
+
config: RobotLab::RunConfig.new(
|
|
775
|
+
auto_compact: :context_window,
|
|
776
|
+
compact_threshold: 0.70
|
|
777
|
+
)
|
|
727
778
|
)
|
|
728
779
|
|
|
729
780
|
# Application-owned compaction: full control over when and how
|
|
730
781
|
robot = RobotLab.build(
|
|
731
782
|
name: "analyst",
|
|
732
783
|
system_prompt: "You are a research analyst.",
|
|
733
|
-
|
|
784
|
+
config: RobotLab::RunConfig.new(
|
|
785
|
+
auto_compact: ->(r) { r.compress_history(recent_turns: 5) if r.chat.messages.size > 40 }
|
|
786
|
+
)
|
|
734
787
|
)
|
|
735
788
|
```
|
|
736
789
|
|
|
@@ -740,12 +793,7 @@ robot = RobotLab.build(
|
|
|
740
793
|
| `:context_window` | Compact when estimated token usage exceeds `compact_threshold` fraction of model's context window |
|
|
741
794
|
| `Proc` | Called with the robot before each `run()`; application decides when and how to compact |
|
|
742
795
|
|
|
743
|
-
`compact_threshold` defaults to `0.80` (80%).
|
|
744
|
-
|
|
745
|
-
```ruby
|
|
746
|
-
config = RobotLab::RunConfig.new(auto_compact: :context_window, compact_threshold: 0.75)
|
|
747
|
-
robot = RobotLab.build(name: "analyst", system_prompt: "...", config: config)
|
|
748
|
-
```
|
|
796
|
+
`compact_threshold` defaults to `0.80` (80%). The built-in `:context_window` strategy uses the optional `classifier` gem; if it is missing, compaction is skipped with a logged warning rather than raising.
|
|
749
797
|
|
|
750
798
|
## Learning Accumulation
|
|
751
799
|
|
|
@@ -763,18 +811,9 @@ reviewer.learn("This codebase prefers map/collect over manual array accumulation
|
|
|
763
811
|
reviewer.run("Review snippet B") # learning is injected automatically
|
|
764
812
|
```
|
|
765
813
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
```ruby
|
|
769
|
-
reviewer = RobotLab.build(
|
|
770
|
-
name: "reviewer",
|
|
771
|
-
system_prompt: "You are a Ruby code reviewer.",
|
|
772
|
-
learn: true,
|
|
773
|
-
learn_domain: "ruby_review"
|
|
774
|
-
)
|
|
775
|
-
```
|
|
814
|
+
Cross-session learning promotion is provided by the [robot_lab-durable](https://github.com/MadBomber/robot_lab-durable) gem, which hooks the `:learn` lifecycle family. See that gem's documentation for setup.
|
|
776
815
|
|
|
777
|
-
Learnings deduplicate bidirectionally: if a broader learning is added that contains an existing narrower one, the narrower one is dropped. Learnings are persisted to the robot's `Memory`
|
|
816
|
+
Learnings deduplicate bidirectionally: if a broader learning is added that contains an existing narrower one, the narrower one is dropped. Learnings are persisted to the robot's `Memory` for the lifetime of that robot.
|
|
778
817
|
|
|
779
818
|
```ruby
|
|
780
819
|
reviewer.learnings # => ["This codebase prefers map/collect..."]
|
|
@@ -783,7 +822,9 @@ reviewer.learn("new fact") # deduplicates before storing
|
|
|
783
822
|
|
|
784
823
|
## Context Window Compression
|
|
785
824
|
|
|
786
|
-
`robot.compress_history` prunes old conversation turns using
|
|
825
|
+
`robot.compress_history` prunes old conversation turns using term-frequency cosine similarity, keeping only turns that are relevant to the most recent context. System messages and tool call/result pairs are always preserved.
|
|
826
|
+
|
|
827
|
+
Scoring uses raw term frequencies without IDF weighting — on a topic-focused conversation, IDF would suppress exactly the recurring terms that signal relevance.
|
|
787
828
|
|
|
788
829
|
```ruby
|
|
789
830
|
# Basic compression: protect the 3 most recent turns, drop unrelated old turns
|
|
@@ -811,7 +852,7 @@ gem "classifier", "~> 2.3"
|
|
|
811
852
|
|
|
812
853
|
## Convergence Detection
|
|
813
854
|
|
|
814
|
-
`RobotLab::Convergence` detects when two independent agents have reached the same conclusion using
|
|
855
|
+
`RobotLab::Convergence` detects when two independent agents have reached the same conclusion using term-frequency cosine similarity (no IDF — on a two-document comparison, IDF collapses the shared terms to zero). Use it to skip an expensive reconciler LLM call when verifiers already agree.
|
|
815
856
|
|
|
816
857
|
```ruby
|
|
817
858
|
# Check similarity directly
|
|
@@ -826,18 +867,30 @@ RobotLab::Convergence.detected?(result_a.reply, result_b.reply)
|
|
|
826
867
|
RobotLab::Convergence.detected?(text_a, text_b, threshold: 0.75)
|
|
827
868
|
```
|
|
828
869
|
|
|
829
|
-
|
|
870
|
+
`similarity` returns `0.0` if either text is blank or shorter than 30 characters (`Convergence::MIN_TEXT_LENGTH`), since term-frequency scoring is not meaningful on very short strings. Short replies like `"yes"` will therefore never register as converged.
|
|
871
|
+
|
|
872
|
+
A common pattern is a gate robot that decides whether reconciliation is needed. Routing is done by overriding `call` and activating an `depends_on: :optional` task — networks have no separate router parameter:
|
|
830
873
|
|
|
831
874
|
```ruby
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
875
|
+
class ConvergenceGate < RobotLab::Robot
|
|
876
|
+
def call(result)
|
|
877
|
+
a = result.context[:verifier_a]&.reply.to_s
|
|
878
|
+
b = result.context[:verifier_b]&.reply.to_s
|
|
879
|
+
|
|
880
|
+
# Converged? Pass the result straight through and leave :reconciler dormant.
|
|
881
|
+
RobotLab::Convergence.detected?(a, b) ? result : result.activate(:reconciler)
|
|
882
|
+
end
|
|
836
883
|
end
|
|
837
884
|
|
|
838
|
-
network = RobotLab.create_network(name: "verify"
|
|
839
|
-
|
|
885
|
+
network = RobotLab.create_network(name: "verify") do
|
|
886
|
+
task :verifier_a, verifier_a, depends_on: :none
|
|
887
|
+
task :verifier_b, verifier_b, depends_on: :none
|
|
888
|
+
task :gate, ConvergenceGate.new(name: "gate"), depends_on: [:verifier_a, :verifier_b]
|
|
889
|
+
task :reconciler, reconciler, depends_on: :optional
|
|
840
890
|
end
|
|
891
|
+
|
|
892
|
+
result = network.run(message: "Audit this change")
|
|
893
|
+
result.context.key?(:reconciler) # => false when the verifiers agreed
|
|
841
894
|
```
|
|
842
895
|
|
|
843
896
|
Requires the `classifier` gem (`~> 2.3`).
|
|
@@ -943,11 +996,18 @@ RobotLab's optional capabilities are packaged as separate gems:
|
|
|
943
996
|
|
|
944
997
|
| Gem | Description |
|
|
945
998
|
|-----|-------------|
|
|
946
|
-
| [robot_lab-
|
|
999
|
+
| [robot_lab-a2a](https://github.com/MadBomber/robot_lab-a2a) | Expose robots and networks as Agent2Agent (A2A) protocol services over HTTP+SSE |
|
|
1000
|
+
| [robot_lab-audit](https://github.com/MadBomber/robot_lab-audit) | SQLite-backed execution audit log wired through the Hook system |
|
|
1001
|
+
| [robot_lab-discovery](https://github.com/MadBomber/robot_lab-discovery) | Zero-configuration mDNS/DNS-SD robot discovery on local networks |
|
|
1002
|
+
| [robot_lab-document_store](https://github.com/MadBomber/robot_lab-document_store) | Embedding-based semantic document store for search / RAG |
|
|
1003
|
+
| [robot_lab-durable](https://github.com/MadBomber/robot_lab-durable) | HTM-backed long-term memory and cross-session knowledge persistence |
|
|
1004
|
+
| [robot_lab-ractor](https://github.com/MadBomber/robot_lab-ractor) | Ractor-based parallel tool execution and DAG-scheduled parallel networks |
|
|
947
1005
|
| [robot_lab-rails](https://github.com/MadBomber/robot_lab-rails) | Rails Engine, generators, `RobotLab::Job` ActiveJob base with Turbo Stream broadcasting |
|
|
948
|
-
| [robot_lab-
|
|
949
|
-
| [robot_lab-
|
|
950
|
-
|
|
1006
|
+
| [robot_lab-to](https://github.com/MadBomber/robot_lab-to) | Autonomous overnight agent loop — iterate a robot toward an objective |
|
|
1007
|
+
| [robot_lab-web](https://github.com/MadBomber/robot_lab-web) | Browser console — stream a robot's run over Server-Sent Events |
|
|
1008
|
+
|
|
1009
|
+
> [!NOTE]
|
|
1010
|
+
> `robot_lab-acp` is retired; its functionality is superseded by `robot_lab-a2a`.
|
|
951
1011
|
|
|
952
1012
|
## Documentation
|
|
953
1013
|
|
data/docs/api/core/index.md
CHANGED
|
@@ -10,16 +10,28 @@ classDiagram
|
|
|
10
10
|
+name: String
|
|
11
11
|
+description: String
|
|
12
12
|
+model: String
|
|
13
|
-
+template:
|
|
14
|
-
+
|
|
15
|
-
+
|
|
13
|
+
+template: Symbol
|
|
14
|
+
+local_tools: Array~Tool~
|
|
15
|
+
+mcp_tools: Array~Tool~
|
|
16
|
+
+tools_config: Symbol_or_Array
|
|
17
|
+
+run(message) RobotResult
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class RobotResult {
|
|
21
|
+
+robot_name: String
|
|
22
|
+
+reply: String
|
|
23
|
+
+output: Array~TextMessage~
|
|
24
|
+
+input_tokens: Integer
|
|
25
|
+
+output_tokens: Integer
|
|
26
|
+
+export() Hash
|
|
16
27
|
}
|
|
17
28
|
|
|
18
29
|
class Network {
|
|
19
30
|
+name: String
|
|
20
31
|
+robots: Hash
|
|
21
32
|
+config: RunConfig
|
|
22
|
-
+
|
|
33
|
+
+memory: Memory
|
|
34
|
+
+run(message) SimpleFlow_Result
|
|
23
35
|
}
|
|
24
36
|
|
|
25
37
|
class RunConfig {
|
|
@@ -32,8 +44,9 @@ classDiagram
|
|
|
32
44
|
class Tool {
|
|
33
45
|
+name: String
|
|
34
46
|
+description: String
|
|
35
|
-
+
|
|
36
|
-
+
|
|
47
|
+
+robot: Robot
|
|
48
|
+
+mcp: String
|
|
49
|
+
+call(args)
|
|
37
50
|
}
|
|
38
51
|
|
|
39
52
|
class Memory {
|
|
@@ -60,22 +73,28 @@ classDiagram
|
|
|
60
73
|
Robot --> RunConfig : uses
|
|
61
74
|
Robot --> Tool : has
|
|
62
75
|
Robot --> Memory : uses
|
|
76
|
+
Robot ..> RobotResult : returns
|
|
63
77
|
Network --> Memory : uses
|
|
64
78
|
Robot ..> RobotMessage : sends/receives
|
|
79
|
+
Memory --> StateProxy : data
|
|
65
80
|
```
|
|
66
81
|
|
|
67
82
|
## Classes
|
|
68
83
|
|
|
69
84
|
| Class | Purpose |
|
|
70
85
|
|-------|---------|
|
|
71
|
-
| [Robot](robot.md) | LLM agent with
|
|
72
|
-
| [
|
|
73
|
-
|
|
|
74
|
-
| [
|
|
86
|
+
| [Robot](robot.md) | LLM agent with templates, tools, memory, and model configuration |
|
|
87
|
+
| [RobotResult](result.md) | Value object returned by `Robot#run` |
|
|
88
|
+
| [Network](network.md) | Container for robots with DAG orchestration |
|
|
89
|
+
| [RunConfig](robot.md#runconfig) | Shared configuration for LLM, tools, callbacks, and infrastructure |
|
|
90
|
+
| [Tool](tool.md) | Callable function with parameters and an `execute` method |
|
|
75
91
|
| [AskUser](tool.md#built-in-askuser) | Built-in tool for terminal-based user interaction |
|
|
76
92
|
| [Memory](memory.md) | Reactive key-value store for sharing data |
|
|
93
|
+
| [StateProxy](state.md) | Hash/method-access wrapper returned by `memory.data` |
|
|
77
94
|
| RobotMessage | Typed envelope for bus-based inter-robot communication |
|
|
78
|
-
| `RobotLab::Runnable` | Shared interface (`crew`, `chief`, `
|
|
95
|
+
| `RobotLab::Runnable` | Shared interface (`crew`, `chief`, `robot_count`, `network?`, `single?`) implemented by both `Robot` and `Network` — see [Runnable Protocol](../../architecture/core-concepts.md#runnable-protocol) |
|
|
96
|
+
|
|
97
|
+
There is no `RobotLab::State` class and no `RobotLab::NetworkRun` class.
|
|
79
98
|
|
|
80
99
|
## Quick Examples
|
|
81
100
|
|
|
@@ -89,18 +108,25 @@ robot = RobotLab.build(
|
|
|
89
108
|
local_tools: [greet_tool]
|
|
90
109
|
)
|
|
91
110
|
|
|
92
|
-
result = robot.run("Hello!")
|
|
111
|
+
result = robot.run("Hello!") # no tools sent — run() defaults to tools: :none
|
|
112
|
+
result = robot.run("Hello!", tools: :inherit) # sends greet_tool
|
|
113
|
+
result.reply # => String
|
|
93
114
|
```
|
|
94
115
|
|
|
116
|
+
`Robot#run` returns a [`RobotLab::RobotResult`](result.md), not a `Message`.
|
|
117
|
+
|
|
95
118
|
### Network
|
|
96
119
|
|
|
120
|
+
The builder DSL method is `task` (there is no `step`):
|
|
121
|
+
|
|
97
122
|
```ruby
|
|
98
123
|
network = RobotLab.create_network(name: "my_network") do
|
|
99
|
-
|
|
100
|
-
|
|
124
|
+
task :analyzer, analyzer_robot, depends_on: :none
|
|
125
|
+
task :writer, writer_robot, depends_on: [:analyzer]
|
|
101
126
|
end
|
|
102
127
|
|
|
103
|
-
result = network.run(message: "Process this")
|
|
128
|
+
result = network.run(message: "Process this") # => SimpleFlow::Result
|
|
129
|
+
result.value # => RobotResult of the last task
|
|
104
130
|
```
|
|
105
131
|
|
|
106
132
|
### Memory
|