robot_lab 0.2.1 → 0.2.6
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/.envrc +1 -0
- data/.loki +60 -0
- data/.quality/reek_baseline.txt +43 -0
- data/.rubocop.yml +5 -167
- data/CHANGELOG.md +54 -0
- data/README.md +61 -0
- data/Rakefile +28 -12
- data/docs/api/core/index.md +1 -0
- data/docs/api/core/network.md +31 -1
- data/docs/api/core/robot.md +91 -5
- data/docs/api/errors.md +47 -2
- data/docs/api/index.md +3 -0
- data/docs/architecture/core-concepts.md +40 -0
- data/docs/architecture/network-orchestration.md +8 -0
- data/docs/architecture/robot-execution.md +1 -1
- data/docs/getting-started/configuration.md +20 -1
- data/docs/guides/building-robots.md +39 -1
- data/docs/guides/creating-networks.md +6 -1
- data/docs/guides/hooks.md +997 -0
- data/docs/guides/index.md +5 -0
- data/docs/guides/knowledge.md +2 -2
- data/docs/guides/observability.md +74 -7
- data/docs/guides/using-tools.md +69 -0
- data/docs/index.md +1 -1
- data/examples/03_network.rb +11 -2
- data/examples/34_agentskills.rb +0 -0
- data/examples/35_hooks.rb +256 -0
- data/examples/README.md +7 -0
- data/examples/common.rb +7 -3
- data/examples/xyzzy.rb +90 -0
- data/lib/robot_lab/agent_skill.rb +5 -4
- data/lib/robot_lab/budget/ledger.rb +98 -0
- data/lib/robot_lab/capabilities.rb +84 -0
- data/lib/robot_lab/config/defaults.yml +10 -0
- data/lib/robot_lab/config.rb +4 -4
- data/lib/robot_lab/error.rb +34 -4
- data/lib/robot_lab/errors.rb +45 -0
- data/lib/robot_lab/hook.rb +79 -0
- data/lib/robot_lab/hook_context.rb +194 -0
- data/lib/robot_lab/hook_registry.rb +55 -0
- data/lib/robot_lab/hooks.rb +87 -0
- data/lib/robot_lab/mcp/connection_poller.rb +2 -2
- data/lib/robot_lab/narrator.rb +87 -0
- data/lib/robot_lab/network.rb +77 -20
- data/lib/robot_lab/robot/budget.rb +89 -0
- data/lib/robot_lab/robot/bus_messaging.rb +76 -12
- data/lib/robot_lab/robot/hooking.rb +56 -0
- data/lib/robot_lab/robot/mcp_management.rb +8 -2
- data/lib/robot_lab/robot/template_rendering.rb +2 -2
- data/lib/robot_lab/robot.rb +211 -88
- data/lib/robot_lab/run_config.rb +24 -5
- data/lib/robot_lab/runnable.rb +51 -0
- data/lib/robot_lab/sandbox/null.rb +13 -0
- data/lib/robot_lab/sandbox/seatbelt.rb +104 -0
- data/lib/robot_lab/sandbox.rb +52 -0
- data/lib/robot_lab/script_tool.rb +60 -2
- data/lib/robot_lab/task.rb +26 -20
- data/lib/robot_lab/tool.rb +52 -11
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +49 -0
- data/mkdocs.yml +1 -0
- metadata +36 -99
- data/examples/temp.md +0 -51
- data/site/404.html +0 -2300
- data/site/api/core/index.html +0 -2706
- data/site/api/core/memory/index.html +0 -3793
- data/site/api/core/network/index.html +0 -3500
- data/site/api/core/robot/index.html +0 -4566
- data/site/api/core/state/index.html +0 -3390
- data/site/api/core/tool/index.html +0 -3843
- data/site/api/index.html +0 -2635
- data/site/api/mcp/client/index.html +0 -3435
- data/site/api/mcp/index.html +0 -2783
- data/site/api/mcp/server/index.html +0 -3252
- data/site/api/mcp/transports/index.html +0 -3352
- data/site/api/messages/index.html +0 -2641
- data/site/api/messages/text-message/index.html +0 -3087
- data/site/api/messages/tool-call-message/index.html +0 -3159
- data/site/api/messages/tool-result-message/index.html +0 -3252
- data/site/api/messages/user-message/index.html +0 -3212
- data/site/api/streaming/context/index.html +0 -3282
- data/site/api/streaming/events/index.html +0 -3347
- data/site/api/streaming/index.html +0 -2738
- data/site/architecture/core-concepts/index.html +0 -3757
- data/site/architecture/index.html +0 -2797
- data/site/architecture/message-flow/index.html +0 -3238
- data/site/architecture/network-orchestration/index.html +0 -3433
- data/site/architecture/robot-execution/index.html +0 -3140
- data/site/architecture/state-management/index.html +0 -3498
- data/site/assets/css/custom.css +0 -56
- data/site/assets/images/favicon.png +0 -0
- data/site/assets/images/robot_lab.jpg +0 -0
- data/site/assets/javascripts/bundle.79ae519e.min.js +0 -16
- data/site/assets/javascripts/bundle.79ae519e.min.js.map +0 -7
- data/site/assets/javascripts/lunr/min/lunr.ar.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.da.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.de.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.du.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.el.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.es.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.fi.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.fr.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.he.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.hi.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.hu.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.hy.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.it.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ja.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.jp.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.kn.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.ko.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.multi.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.nl.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.no.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.pt.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ro.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ru.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.sa.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.sv.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ta.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.te.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.th.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.tr.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.vi.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.zh.min.js +0 -1
- data/site/assets/javascripts/lunr/tinyseg.js +0 -206
- data/site/assets/javascripts/lunr/wordcut.js +0 -6708
- data/site/assets/javascripts/workers/search.2c215733.min.js +0 -42
- data/site/assets/javascripts/workers/search.2c215733.min.js.map +0 -7
- data/site/assets/stylesheets/main.484c7ddc.min.css +0 -1
- data/site/assets/stylesheets/main.484c7ddc.min.css.map +0 -1
- data/site/assets/stylesheets/palette.ab4e12ef.min.css +0 -1
- data/site/assets/stylesheets/palette.ab4e12ef.min.css.map +0 -1
- data/site/concepts/index.html +0 -3455
- data/site/examples/basic-chat/index.html +0 -2880
- data/site/examples/index.html +0 -2907
- data/site/examples/mcp-server/index.html +0 -3018
- data/site/examples/multi-robot-network/index.html +0 -3131
- data/site/examples/rails-application/index.html +0 -3329
- data/site/examples/tool-usage/index.html +0 -3085
- data/site/getting-started/configuration/index.html +0 -3745
- data/site/getting-started/index.html +0 -2572
- data/site/getting-started/installation/index.html +0 -2981
- data/site/getting-started/quick-start/index.html +0 -2942
- data/site/guides/building-robots/index.html +0 -4290
- data/site/guides/creating-networks/index.html +0 -3858
- data/site/guides/index.html +0 -2586
- data/site/guides/mcp-integration/index.html +0 -3581
- data/site/guides/memory/index.html +0 -3586
- data/site/guides/rails-integration/index.html +0 -4019
- data/site/guides/streaming/index.html +0 -3157
- data/site/guides/using-tools/index.html +0 -3802
- data/site/index.html +0 -2671
- data/site/search/search_index.json +0 -1
- data/site/sitemap.xml +0 -183
- data/site/sitemap.xml.gz +0 -0
- data/site/tags.json +0 -1
data/docs/api/core/robot.md
CHANGED
|
@@ -35,6 +35,7 @@ Robot.new(
|
|
|
35
35
|
skills: nil,
|
|
36
36
|
max_tool_rounds: nil,
|
|
37
37
|
token_budget: nil,
|
|
38
|
+
cost_budget: nil,
|
|
38
39
|
temperature: nil,
|
|
39
40
|
top_p: nil,
|
|
40
41
|
top_k: nil,
|
|
@@ -60,7 +61,7 @@ Robot.new(
|
|
|
60
61
|
| `provider` | `String`, `Symbol`, `nil` | `nil` | LLM provider for local providers (e.g., `:ollama`, `:gpustack`). Automatically sets `assume_model_exists: true` |
|
|
61
62
|
| `mcp_servers` | `Array` | `[]` | Legacy MCP server configurations |
|
|
62
63
|
| `mcp` | `Symbol`, `Array` | `:none` | Hierarchical MCP config (`:none`, `:inherit`, or server array) |
|
|
63
|
-
| `tools` | `Symbol`, `Array` | `:none` | Hierarchical tools config (`:none`, `:inherit`, or tool name array) |
|
|
64
|
+
| `tools` | `Symbol`, `Array` | `:none` | Hierarchical tools config (`:none`, `:inherit`, or tool name **array**). Must be tool *names* (String/Symbol) — passing an instance or class raises `ArgumentError` telling you to use `local_tools:` instead. See [Runtime Tool Filtering](../../guides/using-tools.md#runtime-tool-filtering) |
|
|
64
65
|
| `on_tool_call` | `Proc`, `nil` | `nil` | Callback invoked when a tool is called |
|
|
65
66
|
| `on_tool_result` | `Proc`, `nil` | `nil` | Callback invoked when a tool returns a result |
|
|
66
67
|
| `on_content` | `Proc`, `nil` | `nil` | Stored streaming callback invoked with each content chunk (see [Streaming](#streaming)) |
|
|
@@ -68,7 +69,8 @@ Robot.new(
|
|
|
68
69
|
| `bus` | `TypedBus::MessageBus`, `nil` | `nil` | Optional message bus for inter-robot communication |
|
|
69
70
|
| `skills` | `Symbol`, `Array<Symbol>`, `nil` | `nil` | Skill templates to prepend (see [Skills](#skills)) |
|
|
70
71
|
| `max_tool_rounds` | `Integer`, `nil` | `nil` | Circuit breaker: raise `ToolLoopError` after this many tool calls in one `run()` (see [Tool Loop Circuit Breaker](#tool-loop-circuit-breaker)) |
|
|
71
|
-
| `token_budget` | `Integer`, `nil` | `nil` | Raise `InferenceError` if cumulative
|
|
72
|
+
| `token_budget` | `Integer`, `nil` | `nil` | Raise `InferenceError` if cumulative tokens exceed this limit after a call; raise `BudgetExceeded` up front if already exhausted (see [Budgets](#budgets)) |
|
|
73
|
+
| `cost_budget` | `Float`, `nil` | `nil` | Same enforcement as `token_budget`, tracked in cumulative dollar cost instead of tokens (requires provider pricing data) |
|
|
72
74
|
| `config` | `RunConfig`, `nil` | `nil` | Shared config merged with explicit kwargs (see [RunConfig](#runconfig)) |
|
|
73
75
|
| `temperature` | `Float`, `nil` | `nil` | Controls randomness (0.0-1.0) |
|
|
74
76
|
| `top_p` | `Float`, `nil` | `nil` | Nucleus sampling threshold |
|
|
@@ -120,6 +122,7 @@ If `name` is omitted, it defaults to `"robot"`.
|
|
|
120
122
|
| `total_input_tokens` | `Integer` | Cumulative input tokens sent across all `run()` calls |
|
|
121
123
|
| `total_output_tokens` | `Integer` | Cumulative output tokens received across all `run()` calls |
|
|
122
124
|
| `learnings` | `Array<String>` | Accumulated cross-run observations (see [Learning Accumulation](#learning-accumulation)) |
|
|
125
|
+
| `budget_ledger` | `RobotLab::Budget::Ledger`, `nil` | Reserve/reconcile ledger backing `token_budget`/`cost_budget`; `nil` when neither is configured (see [Budgets](#budgets)) |
|
|
123
126
|
|
|
124
127
|
## Attributes (Read-Write)
|
|
125
128
|
|
|
@@ -149,13 +152,15 @@ Primary execution method. Sends a message to the LLM with memory/MCP/tools resol
|
|
|
149
152
|
| `network` | `NetworkRun`, `nil` | `nil` | Network context (passed internally) |
|
|
150
153
|
| `network_memory` | `Memory`, `nil` | `nil` | Shared network memory |
|
|
151
154
|
| `memory` | `Memory`, `Hash`, `nil` | `nil` | Runtime memory to merge |
|
|
152
|
-
| `mcp` | `Symbol`, `Array` | `:none` | Runtime MCP override |
|
|
153
|
-
| `tools` | `Symbol`, `Array` | `:none` | Runtime tools override |
|
|
155
|
+
| `mcp` | `Symbol`, `Array` | `:none` | Runtime MCP override — `:inherit` (all attached servers), `:none`/`[]` (zero this turn), or an explicit array |
|
|
156
|
+
| `tools` | `Symbol`, `Array` | `:none` | Runtime tools override — `:inherit` (all attached tools), `:none`/`[]` (zero this turn), or an explicit name array. See [Runtime Tool Filtering](../../guides/using-tools.md#runtime-tool-filtering) |
|
|
154
157
|
| `**kwargs` | `Hash` | `{}` | Additional keyword arguments passed to `Agent#ask` |
|
|
155
158
|
| `&block` | `Proc` | `nil` | Per-call streaming block, receives each content chunk |
|
|
156
159
|
|
|
157
160
|
When both a stored `on_content` callback and a runtime block are provided, both fire (stored first, then runtime block).
|
|
158
161
|
|
|
162
|
+
Because `tools`/`mcp` default to `:none` here too, a bare `robot.run(message)` with no override sends **zero** tools/MCP servers for that call — pass `tools: :inherit` (and/or `mcp: :inherit`) explicitly to use what's attached. Each call's resolved tool set *replaces* the chat's tools rather than accumulating, so a subsequent `:none` call correctly clears whatever a prior call attached, and the fully-resolved set is clamped to `max_tools` (128 by default) right before being handed to the provider — see [Tool Capping](../../guides/using-tools.md#tool-capping-and-per-turn-filtering).
|
|
163
|
+
|
|
159
164
|
**Returns:** `RobotResult`
|
|
160
165
|
|
|
161
166
|
**Examples:**
|
|
@@ -271,7 +276,7 @@ message = robot.send_message(to: :bob, content: "Tell me a joke.")
|
|
|
271
276
|
# => RobotMessage
|
|
272
277
|
```
|
|
273
278
|
|
|
274
|
-
Publish a message to another robot's bus channel. Increments the internal message counter, creates a `RobotMessage`, tracks it in the outbox, and publishes to the target channel.
|
|
279
|
+
Publish a message to another robot's bus channel. Increments the internal message counter, creates a `RobotMessage`, tracks it in the outbox, and publishes to the target channel. The counter and outbox mutation are synchronized with an internal mutex, so concurrent `send_message`/`send_reply` calls from multiple threads and reply correlation on the poller thread never clobber each other.
|
|
275
280
|
|
|
276
281
|
**Parameters:**
|
|
277
282
|
|
|
@@ -337,6 +342,49 @@ robot.on_message do |delivery, message|
|
|
|
337
342
|
end
|
|
338
343
|
```
|
|
339
344
|
|
|
345
|
+
### respond_to_tasks
|
|
346
|
+
|
|
347
|
+
```ruby
|
|
348
|
+
robot.respond_to_tasks(auto_reply: true) { |message| "the reply content" }
|
|
349
|
+
# => self
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Auto-answer inbound (non-reply) bus tasks: run the block to produce a reply, and send it back to the sender. This is the symmetric counterpart to how a `robot_lab-cyborg` Cyborg answers its human — one call makes any bus member a first-class responder without hand-wiring `on_message` yourself.
|
|
353
|
+
|
|
354
|
+
**Parameters:**
|
|
355
|
+
|
|
356
|
+
| Name | Type | Default | Description |
|
|
357
|
+
|------|------|---------|-------------|
|
|
358
|
+
| `auto_reply` | `Boolean` | `true` | Send the block's result back to the sender via `send_reply` |
|
|
359
|
+
| `&responder` | `Proc` | **required** | Receives the inbound `message`; return the reply content (`nil` means no reply) |
|
|
360
|
+
|
|
361
|
+
**Returns:** `self`
|
|
362
|
+
|
|
363
|
+
Messages that are themselves replies (`message.reply?`) are ignored, so a two-way `respond_to_tasks` conversation between robots does not loop. The responder runs on the bus poller's drain thread, so deliveries to this robot are handled one at a time — a long-running responder delays the next inbound message.
|
|
364
|
+
|
|
365
|
+
```ruby
|
|
366
|
+
bob.respond_to_tasks { |message| "handled: #{message.content}" }
|
|
367
|
+
alice.send_message(to: :bob, content: "ping")
|
|
368
|
+
# bob replies "handled: ping" back to alice automatically
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### serve
|
|
372
|
+
|
|
373
|
+
```ruby
|
|
374
|
+
robot.serve(auto_reply: true)
|
|
375
|
+
# => self
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
The common case of `respond_to_tasks`: run every inbound task through this robot's own `#run` and reply with the result — the one-call way to make a Robot cooperate on the bus the way a Cyborg already does out of the box.
|
|
379
|
+
|
|
380
|
+
```ruby
|
|
381
|
+
bob.serve
|
|
382
|
+
alice.send_message(to: :bob, content: "Tell me a joke.")
|
|
383
|
+
# bob runs "Tell me a joke." through its LLM and replies with the result
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Equivalent to `respond_to_tasks(auto_reply: auto_reply) { |message| run(message.content).reply }` (with Hash-content messages flattened to `"key: value"` lines first).
|
|
387
|
+
|
|
340
388
|
### spawn
|
|
341
389
|
|
|
342
390
|
```ruby
|
|
@@ -349,6 +397,8 @@ child = robot.spawn(
|
|
|
349
397
|
|
|
350
398
|
Create a new robot on the same message bus. If the parent has no bus, one is created automatically and the parent is connected to it.
|
|
351
399
|
|
|
400
|
+
The spawned robot inherits its parent's `model` and `provider` (via `robot.model`/`robot.provider`) so a specialist runs on the same LLM as the robot that spawned it — a robot running on a local Ollama model, for instance, spawns specialists that also target that model rather than falling back to `RobotLab.config.ruby_llm.model` (the global default, typically a cloud model that would fail without credentials). Caller-supplied `model:`/`provider:` in `**options` still override.
|
|
401
|
+
|
|
352
402
|
**Parameters:**
|
|
353
403
|
|
|
354
404
|
| Name | Type | Default | Description |
|
|
@@ -813,6 +863,10 @@ robot = RobotLab.build(
|
|
|
813
863
|
result = robot.run("Hello!")
|
|
814
864
|
```
|
|
815
865
|
|
|
866
|
+
`provider:` is threaded through on every re-application of the effective `RunConfig` — including when a template's front matter is re-rendered mid-run — so a local-provider robot (Ollama, GPUStack, LM Studio) doesn't fall back to RubyLLM's static model registry lookup on later turns and raise a spurious "model not found" error.
|
|
867
|
+
|
|
868
|
+
Some local/thinking-mode models (e.g. `qwen3` on Ollama) route all of their output through reasoning content rather than the normal response text. When `response.content` is `nil`, `result.reply` falls back first to `response.thinking.text` (RubyLLM's extended-thinking text), then to the most recent assistant text from later in *the current turn only* — never a stale reply left over from a previous turn.
|
|
869
|
+
|
|
816
870
|
### Robot with MCP
|
|
817
871
|
|
|
818
872
|
```ruby
|
|
@@ -958,6 +1012,26 @@ robot.reset_token_totals
|
|
|
958
1012
|
puts robot.total_input_tokens # => 0
|
|
959
1013
|
```
|
|
960
1014
|
|
|
1015
|
+
### Budgets
|
|
1016
|
+
|
|
1017
|
+
`token_budget:` and `cost_budget:` turn the counters above into enforceable ceilings, backed by a thread-safe `RobotLab::Budget::Ledger` (`robot.budget_ledger`, `nil` when neither is configured):
|
|
1018
|
+
|
|
1019
|
+
```ruby
|
|
1020
|
+
robot = RobotLab.build(
|
|
1021
|
+
name: "capped",
|
|
1022
|
+
system_prompt: "...",
|
|
1023
|
+
token_budget: 10_000,
|
|
1024
|
+
cost_budget: 0.50
|
|
1025
|
+
)
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
Each `run()` reserves the remaining budget for every configured dimension before the LLM call, and reconciles the reservation with actual usage after:
|
|
1029
|
+
|
|
1030
|
+
- **`RobotLab::BudgetExceeded`** — raised up front when a *prior* call already exhausted a dimension; the new call is refused before it spends anything.
|
|
1031
|
+
- **`RobotLab::InferenceError`** — raised after the call when *this* call's actual usage (from `RobotResult#input_tokens`/`output_tokens`, and the response's reported cost when the provider supports pricing) pushes cumulative usage over budget. This is the same error `token_budget` alone has always raised; `cost_budget` uses the analogous message (`"Cost budget exceeded: $X used, budget is $Y"`).
|
|
1032
|
+
|
|
1033
|
+
See [Budgets](../../guides/observability.md#budgets-token--cost) for the full walkthrough.
|
|
1034
|
+
|
|
961
1035
|
## Tool Loop Circuit Breaker
|
|
962
1036
|
|
|
963
1037
|
Set `max_tool_rounds:` to guard against a robot looping indefinitely through tool calls. After the limit is reached, `RobotLab::ToolLoopError` is raised.
|
|
@@ -1084,6 +1158,18 @@ robot.learn("avoid using puts and p in production code")
|
|
|
1084
1158
|
# => broader learning replaces narrower; robot.learnings.size == 1
|
|
1085
1159
|
```
|
|
1086
1160
|
|
|
1161
|
+
## Runnable Protocol
|
|
1162
|
+
|
|
1163
|
+
`Robot` includes `RobotLab::Runnable`, the shared interface it has in common with `Network` — see [Runnable Protocol](../../architecture/core-concepts.md#runnable-protocol) for the full picture. For a single robot:
|
|
1164
|
+
|
|
1165
|
+
| Method | Returns |
|
|
1166
|
+
|--------|---------|
|
|
1167
|
+
| `crew` | `[self]` — a robot is a crew of one |
|
|
1168
|
+
| `chief` | `self` |
|
|
1169
|
+
| `robot_count` | `1` |
|
|
1170
|
+
| `network?` | `false` |
|
|
1171
|
+
| `single?` | `true` |
|
|
1172
|
+
|
|
1087
1173
|
## See Also
|
|
1088
1174
|
|
|
1089
1175
|
- [Building Robots Guide](../../guides/building-robots.md) (includes [Composable Skills](../../guides/building-robots.md#composable-skills))
|
data/docs/api/errors.md
CHANGED
|
@@ -15,7 +15,8 @@ StandardError
|
|
|
15
15
|
├── RobotLab::MCPError
|
|
16
16
|
├── RobotLab::BusError
|
|
17
17
|
├── RobotLab::RactorBoundaryError
|
|
18
|
-
|
|
18
|
+
├── RobotLab::ToolError
|
|
19
|
+
└── RobotLab::BudgetExceeded
|
|
19
20
|
```
|
|
20
21
|
|
|
21
22
|
Additionally, `DelegationFuture` defines its own scoped error:
|
|
@@ -118,6 +119,8 @@ rescue RobotLab::MCPError => e
|
|
|
118
119
|
end
|
|
119
120
|
```
|
|
120
121
|
|
|
122
|
+
`MCPError.new(message, retryable: true)` marks a specific instance as safe to retry (see [Retryable Errors](#retryable-errors) below). The connection-lost and timeout errors raised internally by `MCP::ConnectionPoller` are always constructed with `retryable: true`.
|
|
123
|
+
|
|
121
124
|
---
|
|
122
125
|
|
|
123
126
|
## RobotLab::BusError
|
|
@@ -160,6 +163,48 @@ rescue RobotLab::ToolError => e
|
|
|
160
163
|
end
|
|
161
164
|
```
|
|
162
165
|
|
|
166
|
+
Like `MCPError`, `ToolError.new(message, retryable: true)` marks a specific instance as retryable. When a tool raises inside `Tool#call`, RobotLab appends `" (retryable)"` to the formatted error text returned to the LLM whenever `RobotLab::Errors.retryable?(error)` is true, so the model itself can see that another attempt is worth trying.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## RobotLab::BudgetExceeded
|
|
171
|
+
|
|
172
|
+
Raised when a Robot's configured `token_budget` or `cost_budget` is already exhausted **before** an LLM call is attempted (see [Budgets](../guides/observability.md#budgets-token--cost) and `RobotLab::Budget::Ledger`). This is distinct from the pre-existing `InferenceError` "Token budget exceeded" message, which covers a call that *completed* but pushed cumulative usage over budget — `BudgetExceeded` means the call was refused outright, before any tokens were spent.
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
robot = RobotLab.build(name: "capped", system_prompt: "...", cost_budget: 0.50)
|
|
176
|
+
|
|
177
|
+
begin
|
|
178
|
+
robot.run("Do the expensive thing")
|
|
179
|
+
rescue RobotLab::BudgetExceeded => e
|
|
180
|
+
puts e.message # "budget exceeded for cost: 0.62 > 0.5"
|
|
181
|
+
end
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Retryable Errors
|
|
187
|
+
|
|
188
|
+
`RobotLab::Errors.retryable?(error)` classifies whether a host (an ActiveJob `retry_on` list, `robot_lab-to`'s takeover loop, a custom retry wrapper) should retry the operation that raised `error`:
|
|
189
|
+
|
|
190
|
+
| Error | Retryable? |
|
|
191
|
+
|-------|------------|
|
|
192
|
+
| `InferenceError` (and subclasses, except `ToolLoopError`) | Always |
|
|
193
|
+
| `ToolLoopError` | Never — it's a circuit breaker; retrying immediately re-triggers the same loop |
|
|
194
|
+
| `MCPError` / `ToolError` | Only when raised with `retryable: true` at the raise site |
|
|
195
|
+
| Everything else (`ConfigurationError`, `ToolNotFoundError`, `DependencyError`, `RactorBoundaryError`, `BusError`, `BudgetExceeded`, non-RobotLab errors) | Never |
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
begin
|
|
199
|
+
robot.run(task)
|
|
200
|
+
rescue RobotLab::Error => e
|
|
201
|
+
retry if RobotLab::Errors.retryable?(e)
|
|
202
|
+
raise
|
|
203
|
+
end
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`RobotLab::Errors.retryable_classes` returns `[RobotLab::InferenceError]` — an always-retryable allow-list for explicit ActiveJob-style `retry_on` declarations. It excludes `MCPError`/`ToolError` because their retryability is per-raise (via `retryable:`), not per-class.
|
|
207
|
+
|
|
163
208
|
---
|
|
164
209
|
|
|
165
210
|
## RobotLab::DelegationFuture::DelegationTimeout
|
|
@@ -181,5 +226,5 @@ end
|
|
|
181
226
|
## Related
|
|
182
227
|
|
|
183
228
|
- [Building Robots](../guides/building-robots.md) — Tool loop circuit breaker, delegation
|
|
184
|
-
- [
|
|
229
|
+
- [Observability & Safety](../guides/observability.md) — circuit breaker and tool loop errors
|
|
185
230
|
- [MCP Integration](../guides/mcp-integration.md) — MCP connection errors
|
data/docs/api/index.md
CHANGED
|
@@ -56,6 +56,9 @@ RobotLab.reload_config! # => reload from all sources
|
|
|
56
56
|
RobotLab.build(name:, template:, system_prompt:, context:, **options)
|
|
57
57
|
RobotLab.create_network(name:, concurrency:) { ... }
|
|
58
58
|
RobotLab.create_memory(data:, enable_cache:, **options)
|
|
59
|
+
|
|
60
|
+
# Rendering a template to a String (not a robot) -- see Building Robots guide
|
|
61
|
+
RobotLab.render_template(name, **context) # => String
|
|
59
62
|
```
|
|
60
63
|
|
|
61
64
|
See individual class documentation for detailed method references.
|
|
@@ -415,6 +415,16 @@ end
|
|
|
415
415
|
|
|
416
416
|
Block arity controls delivery handling: 1 argument auto-acks; 2 arguments give manual control over `delivery.ack!`/`delivery.nack!`.
|
|
417
417
|
|
|
418
|
+
`send_message`/`send_reply` synchronize the per-robot message counter and outbox with an internal mutex, so concurrent sends from multiple threads and reply correlation on the poller thread can't clobber each other — the bus is safe to send on from more than one thread at a time.
|
|
419
|
+
|
|
420
|
+
For the common case of a robot that should simply answer whatever tasks arrive on the bus, `respond_to_tasks`/`serve` do the `on_message` wiring above in one call:
|
|
421
|
+
|
|
422
|
+
```ruby
|
|
423
|
+
bob.serve # equivalent to the on_message block above, running bob.run and replying automatically
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
`respond_to_tasks` takes a block instead when the reply needs post-processing, and both ignore inbound messages that are themselves replies, so two robots calling `serve` on each other don't loop. See [Auto-Responding to Bus Tasks](../guides/building-robots.md#auto-responding-to-bus-tasks).
|
|
427
|
+
|
|
418
428
|
### Dynamic Spawning
|
|
419
429
|
|
|
420
430
|
Robots can create new robots at runtime using `spawn`. The bus is created lazily — no upfront wiring required:
|
|
@@ -430,6 +440,8 @@ answer = helper.run("What is 2+2?").last_text_content
|
|
|
430
440
|
helper.send_message(to: :dispatcher, content: answer)
|
|
431
441
|
```
|
|
432
442
|
|
|
443
|
+
The spawned robot inherits its parent's `model`/`provider`, so a dispatcher running on a local provider (e.g. Ollama) spawns specialists targeting that same model instead of falling back to the global default (which would fail without cloud credentials). Explicit `model:`/`provider:` passed to `spawn` still override.
|
|
444
|
+
|
|
433
445
|
Robots can also join a bus after creation using `with_bus`:
|
|
434
446
|
|
|
435
447
|
```ruby
|
|
@@ -480,6 +492,34 @@ Networks provide:
|
|
|
480
492
|
- **Per-task configuration** via the `Task` wrapper
|
|
481
493
|
- **Broadcast messaging** for network-wide announcements
|
|
482
494
|
|
|
495
|
+
Robots can be added to a network without a pipeline task via `add_robot`, and removed again with `remove_robot(name)` (returns the removed robot, or `nil` if absent). `remove_robot` only drops the robot from the crew — it doesn't rewrite the pipeline, so avoid removing a robot that's still referenced by a task's `depends_on`.
|
|
496
|
+
|
|
497
|
+
## Runnable Protocol
|
|
498
|
+
|
|
499
|
+
`RobotLab::Runnable` is a shared interface implemented by both `Robot` and `Network`, so callers can treat either uniformly instead of branching on `is_a?(RobotLab::Network)`:
|
|
500
|
+
|
|
501
|
+
```ruby
|
|
502
|
+
def summarize(runnable)
|
|
503
|
+
runnable.crew.each { |r| puts r.name }
|
|
504
|
+
puts "chief: #{runnable.chief.name}"
|
|
505
|
+
puts runnable.network? ? "network of #{runnable.robot_count}" : "single robot"
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
summarize(robot) # crew: [robot], chief: robot, "single robot"
|
|
509
|
+
summarize(network) # crew: network.robots.values, chief: crew.first, "network of N"
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
| Method | Robot | Network |
|
|
513
|
+
|--------|-------|---------|
|
|
514
|
+
| `crew` | `[self]` | `robots.values` (pipeline order) |
|
|
515
|
+
| `chief` | `self` | `crew.first` |
|
|
516
|
+
| `robot_count` | `1` | `crew.size` |
|
|
517
|
+
| `network?` | `false` | `true` |
|
|
518
|
+
| `single?` | `true` | `false` |
|
|
519
|
+
| `run(message = nil, **opts)` | accepts a positional message (already did) | now also accepts a positional message, folded into `message:` — the keyword form still works |
|
|
520
|
+
|
|
521
|
+
`crew` is the only method implementers must define themselves; `chief`, `robot_count`, and `single?` all derive from it, and `network?` defaults to `false` unless overridden (as `Network` does).
|
|
522
|
+
|
|
483
523
|
## Next Steps
|
|
484
524
|
|
|
485
525
|
- [Robot Execution](robot-execution.md) - Detailed execution flow
|
|
@@ -19,6 +19,10 @@ network = RobotLab.create_network(name: "customer_service") do
|
|
|
19
19
|
end
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
## Runnable Protocol
|
|
23
|
+
|
|
24
|
+
`Network` implements `RobotLab::Runnable` — the same interface `Robot` implements — so code that needs to run "a robot or a network" doesn't have to branch on `is_a?(RobotLab::Network)`. For a `Network`: `crew` returns `robots.values` (pipeline order), `chief` is `crew.first`, `robot_count` is `crew.size`, and `network?` is `true`. `run(message = nil, **opts)` accepts a positional message the same way `Robot#run` does — it's folded into `message:` — while the existing `run(message: ...)` keyword form still works unchanged. See [Runnable Protocol](../architecture/core-concepts.md#runnable-protocol) in Core Concepts for the full comparison against `Robot`.
|
|
25
|
+
|
|
22
26
|
## Creating Networks
|
|
23
27
|
|
|
24
28
|
Networks are created via `RobotLab.create_network` with a block DSL:
|
|
@@ -352,6 +356,10 @@ network.available_robots #=> [Robot, Robot, ...]
|
|
|
352
356
|
# Add a robot without a task
|
|
353
357
|
network.add_robot(extra_robot)
|
|
354
358
|
|
|
359
|
+
# Remove a dynamically-added robot (returns it, or nil if absent).
|
|
360
|
+
# Only affects the crew (@robots) -- does not rewrite the pipeline.
|
|
361
|
+
network.remove_robot(:extra_robot)
|
|
362
|
+
|
|
355
363
|
# Convert to hash
|
|
356
364
|
network.to_h
|
|
357
365
|
#=> { name: "support", robots: ["classifier", "billing"], tasks: [...], optional_tasks: [...] }
|
|
@@ -294,7 +294,7 @@ def call(result)
|
|
|
294
294
|
.with_context(@name.to_sym, robot_result)
|
|
295
295
|
.continue(robot_result)
|
|
296
296
|
rescue Exception => e
|
|
297
|
-
|
|
297
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
298
298
|
error_result = RobotResult.new(
|
|
299
299
|
robot_name: @name,
|
|
300
300
|
output: [TextMessage.new(role: 'assistant', content: "Error: #{e.class}: #{e.message}")]
|
|
@@ -199,6 +199,20 @@ Default chat parameters applied to all robots unless overridden:
|
|
|
199
199
|
| `chat.with_params.frequency_penalty` | `null` | Frequency penalty (-2.0 to 2.0) |
|
|
200
200
|
| `chat.with_params.stop` | `null` | Stop sequences |
|
|
201
201
|
|
|
202
|
+
### Skill-Script Sandboxing (`sandbox:` section)
|
|
203
|
+
|
|
204
|
+
Opt-in confinement for the scripts a [skill bundle](../guides/using-tools.md#skill-scripts-and-sandboxing) exposes as tools. Disabled by default — scripts run exactly as before until you turn it on:
|
|
205
|
+
|
|
206
|
+
| Key | Default | Description |
|
|
207
|
+
|-----|---------|-------------|
|
|
208
|
+
| `sandbox.enabled` | `false` | Turn on OS-level confinement for skill scripts |
|
|
209
|
+
| `sandbox.fs_read` | `["."]` | Ceiling: paths any skill script may read (relative to cwd) |
|
|
210
|
+
| `sandbox.fs_write` | `[]` | Ceiling: paths any skill script may write |
|
|
211
|
+
| `sandbox.network` | `false` | Ceiling: whether any skill script may use the network |
|
|
212
|
+
| `sandbox.timeout` | `60` | Ceiling: max seconds a skill script may run |
|
|
213
|
+
|
|
214
|
+
These are a **ceiling**, not a grant — the actual permissions a script runs with are the intersection of this ceiling and what the individual SKILL.md declares for itself. See [Skill Scripts and Sandboxing](../guides/using-tools.md#skill-scripts-and-sandboxing) for how the two combine and which platforms actually enforce confinement.
|
|
215
|
+
|
|
202
216
|
## Runtime-Only Attributes
|
|
203
217
|
|
|
204
218
|
Some attributes can only be set at runtime, not through config files. Use direct assignment on `RobotLab.config` or the `RobotLab.configure` block:
|
|
@@ -374,7 +388,9 @@ effective.temperature #=> 0.9 (overridden)
|
|
|
374
388
|
| **LLM** | `model`, `temperature`, `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop` |
|
|
375
389
|
| **Tools** | `mcp`, `tools` |
|
|
376
390
|
| **Callbacks** | `on_tool_call`, `on_tool_result` |
|
|
377
|
-
| **Infrastructure** | `bus`, `enable_cache`, `max_tool_rounds`, `token_budget`, `ractor_pool_size`, `max_concurrent_robots`, `doom_loop_threshold`, `auto_compact`, `compact_threshold` |
|
|
391
|
+
| **Infrastructure** | `bus`, `enable_cache`, `max_tool_rounds`, `token_budget`, `cost_budget`, `max_tools`, `ractor_pool_size`, `max_concurrent_robots`, `doom_loop_threshold`, `auto_compact`, `compact_threshold` |
|
|
392
|
+
|
|
393
|
+
`cost_budget` mirrors `token_budget` for cumulative dollar spend — see [Budgets](../guides/observability.md#budgets-token--cost). `max_tools` overrides the default 128-tool ceiling enforced on every turn before tools are handed to the provider — see [Tool Capping](../guides/using-tools.md#tool-capping-and-per-turn-filtering).
|
|
378
394
|
|
|
379
395
|
### RunConfig vs RobotLab.config
|
|
380
396
|
|
|
@@ -425,10 +441,13 @@ robot = RobotLab.build(
|
|
|
425
441
|
name: "calculator",
|
|
426
442
|
system_prompt: "You solve math problems.",
|
|
427
443
|
mcp: :none,
|
|
444
|
+
tools: :inherit, # required: without this, local_tools below is never sent
|
|
428
445
|
local_tools: [Calculator]
|
|
429
446
|
)
|
|
430
447
|
```
|
|
431
448
|
|
|
449
|
+
> **`tools:` defaults to `:none` at every level**, including `run()` itself. Attaching tools via `local_tools:` does not by itself make them available — an explicit `:none` (or an unset runtime override, which is equivalent) now sends **zero** tools for that call. Pass `tools: :inherit` wherever you want a robot's attached tools to actually be used. See [Runtime Tool Filtering](../guides/using-tools.md#runtime-tool-filtering) for the full `:inherit`/`:none`/array semantics.
|
|
450
|
+
|
|
432
451
|
## Next Steps
|
|
433
452
|
|
|
434
453
|
- [Building Robots](../guides/building-robots.md) - Create custom robots
|
|
@@ -63,7 +63,9 @@ robot = RobotLab.build(
|
|
|
63
63
|
)
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
When `provider:` is set, `assume_model_exists: true` is automatically applied. The provider is available via `robot.provider`.
|
|
66
|
+
When `provider:` is set, `assume_model_exists: true` is automatically applied. The provider is available via `robot.provider`. This context is preserved across every re-application of the robot's config — including when a template's front matter re-renders mid-run — so a local-provider robot doesn't fall back to RubyLLM's static model registry (and raise a "model not found" error) on later turns.
|
|
67
|
+
|
|
68
|
+
Some local models route their entire response through reasoning/thinking content instead of the normal response text (e.g. `qwen3` on Ollama). When that happens, `result.reply` falls back to the thinking text automatically — see [Robot with Local Provider](../api/core/robot.md#robot-with-local-provider) for details.
|
|
67
69
|
|
|
68
70
|
### System Prompt
|
|
69
71
|
|
|
@@ -144,6 +146,17 @@ The following YAML front matter keys are applied to the robot's chat automatical
|
|
|
144
146
|
|
|
145
147
|
Constructor-provided values always take precedence over frontmatter values.
|
|
146
148
|
|
|
149
|
+
### Rendering a Template to a String
|
|
150
|
+
|
|
151
|
+
`RobotLab.render_template` renders a named template directly to a `String` — using the same configured template library (`prompts_dir` / `ROBOT_LAB_TEMPLATE_PATH`) as `template:` on `RobotLab.build` — without constructing a robot. Front-matter parameters are supplied as keyword arguments:
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
RobotLab.render_template(:objective, topic: "commit messages")
|
|
155
|
+
# => "<rendered body of prompts/objective.md, with topic substituted>"
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Unlike `template:` on `build` (which renders a template as a robot's *system prompt*), this returns the plain text — useful as a one-off task message, an evaluation rubric, or any other place you want a parameterized `.md` template's body without the overhead of a robot.
|
|
159
|
+
|
|
147
160
|
### Self-Contained Templates
|
|
148
161
|
|
|
149
162
|
Templates can declare everything a robot needs — identity, tools, MCP servers, and LLM config — making the `.md` file a complete robot definition:
|
|
@@ -686,6 +699,30 @@ The `on_message` block arity controls delivery handling:
|
|
|
686
699
|
|
|
687
700
|
See [Message Bus](../architecture/core-concepts.md#message-bus) for details.
|
|
688
701
|
|
|
702
|
+
### Auto-Responding to Bus Tasks
|
|
703
|
+
|
|
704
|
+
The `Comedian`/`ComedyCritic` example above hand-wires `on_message` to run and reply. For the common case — run every inbound task through the robot and reply with the result — `respond_to_tasks`/`serve` do it in one call:
|
|
705
|
+
|
|
706
|
+
```ruby
|
|
707
|
+
bob = RobotLab.build(name: "bob", template: :comedian, bus: bus)
|
|
708
|
+
bob.serve # every inbound task runs through bob.run and replies automatically
|
|
709
|
+
|
|
710
|
+
alice.send_message(to: :bob, content: "Tell me a funny robot joke.")
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
`serve` is shorthand for `respond_to_tasks(auto_reply: true) { |message| run(message.content).reply }`. Use `respond_to_tasks` directly when the reply shouldn't just be `run(...).reply` — e.g. post-processing the result first:
|
|
714
|
+
|
|
715
|
+
```ruby
|
|
716
|
+
bob.respond_to_tasks do |message|
|
|
717
|
+
joke = run(message.content.to_s).reply.strip
|
|
718
|
+
joke.end_with?("!") ? joke : "#{joke}!"
|
|
719
|
+
end
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
Both ignore messages that are themselves replies (`message.reply?`), so two robots calling `serve`/`respond_to_tasks` on each other don't loop.
|
|
723
|
+
|
|
724
|
+
See [Message Bus](../architecture/core-concepts.md#message-bus) for details.
|
|
725
|
+
|
|
689
726
|
### Spawning Robots Dynamically
|
|
690
727
|
|
|
691
728
|
Create new robots at runtime using `spawn`. The bus is created lazily — no upfront wiring required:
|
|
@@ -728,6 +765,7 @@ Key features of `spawn`:
|
|
|
728
765
|
- Creates a bus lazily if the parent doesn't have one
|
|
729
766
|
- Spawned robots can immediately send and receive messages
|
|
730
767
|
- Multiple robots with the same name enable fan-out messaging
|
|
768
|
+
- The child inherits the parent's `model`/`provider` (caller-supplied `model:`/`provider:` still override) — a dispatcher running on a local Ollama model spawns specialists that also target that model, instead of falling back to the global default model/provider
|
|
731
769
|
|
|
732
770
|
Robots can also join a bus after creation:
|
|
733
771
|
|
|
@@ -105,7 +105,7 @@ network = RobotLab.create_network(name: "launch_assessment", config: config) do
|
|
|
105
105
|
end
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
`nil` (the default) means unlimited — identical to pre-existing behavior. For Rails deployments, size the cap to match your database connection pool and API rate tier. See [
|
|
108
|
+
`nil` (the default) means unlimited — identical to pre-existing behavior. For Rails deployments, size the cap to match your database connection pool and API rate tier. See [examples/31_launch_assessment.rb](https://github.com/MadBomber/robot_lab/blob/main/examples/31_launch_assessment.rb) for a working demo.
|
|
109
109
|
|
|
110
110
|
### Optional Tasks
|
|
111
111
|
|
|
@@ -441,8 +441,13 @@ network["billing"] # => Robot instance (alias)
|
|
|
441
441
|
network.available_robots # => Array of Robot instances
|
|
442
442
|
network.memory # => Memory instance (shared)
|
|
443
443
|
network.to_h # => Hash representation
|
|
444
|
+
|
|
445
|
+
network.add_robot(extra_robot) # add without a pipeline task -> self
|
|
446
|
+
network.remove_robot(:extra_robot) # remove by name -> the removed Robot, or nil
|
|
444
447
|
```
|
|
445
448
|
|
|
449
|
+
`remove_robot` only drops the robot from the crew — it doesn't touch the pipeline, so don't remove a robot that's still a `depends_on` target of a task.
|
|
450
|
+
|
|
446
451
|
## Configuration Inheritance
|
|
447
452
|
|
|
448
453
|
Networks accept a `config:` parameter that establishes default LLM settings for all member robots. This is useful when you want consistent behavior across a pipeline without configuring each robot individually.
|