robot_lab 0.2.6 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +124 -64
- data/docs/api/core/index.md +41 -15
- data/docs/api/core/memory.md +247 -29
- data/docs/api/core/network.md +255 -33
- data/docs/api/core/result.md +120 -32
- data/docs/api/core/robot.md +551 -61
- data/docs/api/core/state.md +87 -197
- data/docs/api/core/tool.md +165 -20
- data/docs/api/errors.md +110 -17
- data/docs/api/hooks.md +469 -0
- data/docs/api/index.md +80 -7
- data/docs/api/mcp/client.md +129 -35
- data/docs/api/mcp/index.md +164 -23
- data/docs/api/mcp/server.md +27 -3
- data/docs/api/mcp/transports.md +94 -22
- data/docs/api/messages/index.md +26 -3
- data/docs/api/messages/text-message.md +33 -11
- data/docs/api/messages/tool-call-message.md +27 -4
- data/docs/api/messages/tool-result-message.md +23 -4
- data/docs/api/messages/user-message.md +45 -8
- data/docs/api/skills.md +519 -0
- data/docs/api/streaming/context.md +28 -5
- data/docs/api/streaming/index.md +57 -11
- data/docs/api/support.md +846 -0
- data/docs/architecture/core-concepts.md +79 -31
- data/docs/architecture/index.md +86 -11
- data/docs/architecture/message-flow.md +66 -29
- data/docs/architecture/network-orchestration.md +145 -38
- data/docs/architecture/robot-execution.md +172 -90
- data/docs/architecture/state-management.md +31 -12
- data/docs/concepts.md +176 -21
- data/docs/examples/basic-chat.md +72 -19
- data/docs/examples/index.md +117 -31
- data/docs/examples/mcp-server.md +154 -45
- data/docs/examples/multi-robot-network.md +91 -21
- data/docs/examples/tool-usage.md +104 -37
- data/docs/getting-started/configuration.md +284 -97
- data/docs/getting-started/installation.md +53 -41
- data/docs/getting-started/quick-start.md +51 -6
- data/docs/guides/building-robots.md +258 -50
- data/docs/guides/creating-networks.md +214 -30
- data/docs/guides/hooks.md +141 -54
- data/docs/guides/knowledge.md +35 -4
- data/docs/guides/mcp-integration.md +211 -44
- data/docs/guides/memory.md +103 -12
- data/docs/guides/observability.md +95 -47
- data/docs/guides/streaming.md +184 -125
- data/docs/guides/using-tools.md +237 -17
- data/docs/index.md +36 -4
- data/examples/01_simple_robot.rb +2 -2
- data/examples/02_tools.rb +14 -4
- data/examples/03_network.rb +12 -7
- data/examples/04_mcp.rb +11 -4
- data/examples/05_streaming.rb +8 -5
- data/examples/06_prompt_templates.rb +13 -9
- data/examples/07_network_memory.rb +5 -5
- data/examples/08_llm_config.rb +20 -15
- data/examples/09_chaining.rb +4 -4
- data/examples/11_network_introspection.rb +4 -4
- data/examples/12_message_bus.rb +2 -2
- data/examples/13_spawn.rb +2 -2
- data/examples/14_rusty_circuit/README.md +1 -0
- data/examples/14_rusty_circuit/comic.rb +7 -3
- data/examples/14_rusty_circuit/display.rb +14 -0
- data/examples/14_rusty_circuit/heckler.rb +8 -6
- data/examples/14_rusty_circuit/open_mic.rb +17 -6
- data/examples/14_rusty_circuit/scout.rb +17 -10
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +14 -10
- data/examples/15_memory_network_and_bus/linux_writer.rb +2 -2
- data/examples/15_memory_network_and_bus/os_editor.rb +3 -1
- data/examples/15_memory_network_and_bus/os_writer.rb +4 -1
- data/examples/16_writers_room/writer.rb +22 -22
- data/examples/16_writers_room/writers_room.rb +2 -0
- data/examples/17_skills.rb +14 -13
- data/examples/18_rails/README.md +20 -1
- data/examples/18_rails/app/controllers/chat_controller.rb +5 -1
- data/examples/18_rails/app/jobs/robot_run_job.rb +11 -5
- data/examples/18_rails/app/robots/chat_robot.rb +11 -0
- data/examples/18_rails/config/initializers/robot_lab.rb +8 -0
- data/examples/19_token_tracking.rb +25 -9
- data/examples/20_circuit_breaker.rb +10 -7
- data/examples/21_learning_loop.rb +42 -16
- data/examples/22_context_compression.rb +23 -23
- data/examples/23_convergence.rb +24 -17
- data/examples/24_structured_delegation.rb +13 -8
- data/examples/25_history_search.rb +12 -8
- data/examples/27_incident_response/incident_response.rb +31 -13
- data/examples/28_mcp_discovery.rb +17 -13
- data/examples/29_ractor_tools.rb +4 -2
- data/examples/30_ractor_network.rb +22 -17
- data/examples/31_launch_assessment.rb +20 -9
- data/examples/32_newsletter_reader.rb +7 -2
- data/examples/33_stock_predictor.rb +34 -13
- data/examples/34_agentskills.rb +7 -3
- data/examples/35_hooks.rb +18 -8
- data/examples/README.md +199 -45
- data/examples/common.rb +79 -11
- data/examples/xyzzy.rb +8 -1
- data/lib/robot_lab/config.rb +10 -5
- data/lib/robot_lab/names.rb +402 -0
- data/lib/robot_lab/robot/agent_skill_matching.rb +1 -3
- data/lib/robot_lab/robot/bus_messaging.rb +16 -8
- data/lib/robot_lab/robot/template_rendering.rb +16 -3
- data/lib/robot_lab/robot.rb +23 -2
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +21 -15
- data/mkdocs.yml +6 -1
- metadata +7 -2
data/docs/api/core/robot.md
CHANGED
|
@@ -12,11 +12,22 @@ RubyLLM::Agent
|
|
|
12
12
|
|
|
13
13
|
`Robot` inherits from `RubyLLM::Agent`, which creates a persistent `@chat` on initialization. The robot adds template-based prompts, shared memory, hierarchical MCP configuration, and SimpleFlow pipeline integration on top of the base agent.
|
|
14
14
|
|
|
15
|
+
`Robot` also includes `RobotLab::Runnable` and the mixins
|
|
16
|
+
`Robot::TemplateRendering`, `Robot::MCPManagement`, `Robot::BusMessaging`,
|
|
17
|
+
`Robot::HistorySearch`, `Robot::Budget`, and `Robot::Hooking`, and prepends
|
|
18
|
+
`Robot::AgentSkillMatching`.
|
|
19
|
+
|
|
20
|
+
## Constants
|
|
21
|
+
|
|
22
|
+
| Constant | Value | Description |
|
|
23
|
+
|----------|-------|-------------|
|
|
24
|
+
| `Robot::DEFAULT_MAX_TOOLS` | `128` | Ceiling on the number of tools handed to the provider per turn. Override per robot with `RunConfig#max_tools`; a nil, zero, or negative `max_tools` falls back to this default, so the cap cannot be disabled |
|
|
25
|
+
|
|
15
26
|
## Constructor
|
|
16
27
|
|
|
17
28
|
```ruby
|
|
18
29
|
Robot.new(
|
|
19
|
-
name:,
|
|
30
|
+
name:, # required
|
|
20
31
|
template: nil,
|
|
21
32
|
system_prompt: nil,
|
|
22
33
|
context: {},
|
|
@@ -33,9 +44,6 @@ Robot.new(
|
|
|
33
44
|
enable_cache: true,
|
|
34
45
|
bus: nil,
|
|
35
46
|
skills: nil,
|
|
36
|
-
max_tool_rounds: nil,
|
|
37
|
-
token_budget: nil,
|
|
38
|
-
cost_budget: nil,
|
|
39
47
|
temperature: nil,
|
|
40
48
|
top_p: nil,
|
|
41
49
|
top_k: nil,
|
|
@@ -43,15 +51,27 @@ Robot.new(
|
|
|
43
51
|
presence_penalty: nil,
|
|
44
52
|
frequency_penalty: nil,
|
|
45
53
|
stop: nil,
|
|
54
|
+
max_tool_rounds: nil,
|
|
55
|
+
token_budget: nil,
|
|
56
|
+
cost_budget: nil,
|
|
57
|
+
doom_loop_threshold: nil,
|
|
58
|
+
mcp_discovery: false,
|
|
46
59
|
config: nil
|
|
47
60
|
)
|
|
48
61
|
```
|
|
49
62
|
|
|
63
|
+
!!! warning "The keyword list is closed"
|
|
64
|
+
`Robot#initialize` has no `**rest`. Any keyword not listed above raises
|
|
65
|
+
`ArgumentError`. In particular `auto_compact:`, `compact_threshold:`,
|
|
66
|
+
`ractor_pool_size:`, `max_concurrent_robots:`, and `max_tools:` are
|
|
67
|
+
**`RunConfig` fields only** — pass them via `config:`, not as constructor
|
|
68
|
+
kwargs. There is no `memory:`, `learn:`, or `learn_domain:` keyword.
|
|
69
|
+
|
|
50
70
|
### Parameters
|
|
51
71
|
|
|
52
72
|
| Name | Type | Default | Description |
|
|
53
73
|
|------|------|---------|-------------|
|
|
54
|
-
| `name` | `String` | **required** |
|
|
74
|
+
| `name` | `String` | **required** | Identifier for the robot. `RobotLab.build` defaults it to the literal string `"robot"`; that default is load-bearing — front-matter `robot_name:` is applied only when the constructor name is still `"robot"` |
|
|
55
75
|
| `template` | `Symbol`, `nil` | `nil` | Prompt template (e.g., `:assistant` loads `prompts/assistant.md`) |
|
|
56
76
|
| `system_prompt` | `String`, `nil` | `nil` | Inline system prompt (appended after template if both given) |
|
|
57
77
|
| `context` | `Hash`, `Proc` | `{}` | Variables passed to the template |
|
|
@@ -61,7 +81,7 @@ Robot.new(
|
|
|
61
81
|
| `provider` | `String`, `Symbol`, `nil` | `nil` | LLM provider for local providers (e.g., `:ollama`, `:gpustack`). Automatically sets `assume_model_exists: true` |
|
|
62
82
|
| `mcp_servers` | `Array` | `[]` | Legacy MCP server configurations |
|
|
63
83
|
| `mcp` | `Symbol`, `Array` | `:none` | Hierarchical MCP config (`:none`, `:inherit`, or server array) |
|
|
64
|
-
| `tools` | `Symbol`, `Array` | `:none` | Hierarchical tools config (`:none`, `:inherit`, or tool name **array**). Must be tool *names* (String/Symbol) —
|
|
84
|
+
| `tools` | `Symbol`, `Array` | `:none` | Hierarchical tools config (`:none`, `:inherit`, or tool name **array**). Must be tool *names* (String/Symbol) — `validate_tools_filter!` raises `ArgumentError` for an instance or class, telling you to use `local_tools:` instead. **For a standalone robot, leave this unset at build time**: `tools: :inherit` here resolves against the global parent `:none` and yields an allowlist that matches nothing. Inside a network whose `config:` sets `tools:`, build-time `:inherit` is the correct way to opt in. See [Runtime Tool Filtering](../../guides/using-tools.md#runtime-tool-filtering) |
|
|
65
85
|
| `on_tool_call` | `Proc`, `nil` | `nil` | Callback invoked when a tool is called |
|
|
66
86
|
| `on_tool_result` | `Proc`, `nil` | `nil` | Callback invoked when a tool returns a result |
|
|
67
87
|
| `on_content` | `Proc`, `nil` | `nil` | Stored streaming callback invoked with each content chunk (see [Streaming](#streaming)) |
|
|
@@ -71,17 +91,25 @@ Robot.new(
|
|
|
71
91
|
| `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)) |
|
|
72
92
|
| `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
93
|
| `cost_budget` | `Float`, `nil` | `nil` | Same enforcement as `token_budget`, tracked in cumulative dollar cost instead of tokens (requires provider pricing data) |
|
|
94
|
+
| `doom_loop_threshold` | `Integer`, `nil` | `nil` | Tunes the always-on doom-loop detector (default threshold 3). See [Doom Loop Detection](#doom-loop-detection) |
|
|
95
|
+
| `mcp_discovery` | `Boolean` | `false` | When true, the first run narrows the configured MCP server list to those `MCP::ServerDiscovery` judges relevant to the message |
|
|
74
96
|
| `config` | `RunConfig`, `nil` | `nil` | Shared config merged with explicit kwargs (see [RunConfig](#runconfig)) |
|
|
75
|
-
| `temperature` | `Float`, `nil` | `nil` | Controls randomness
|
|
76
|
-
| `top_p` | `Float`, `nil` | `nil` | Nucleus sampling threshold |
|
|
77
|
-
| `top_k` | `Integer`, `nil` | `nil` | Top-k sampling |
|
|
78
|
-
| `max_tokens` | `Integer`, `nil` | `nil` | Maximum tokens in response |
|
|
79
|
-
| `presence_penalty` | `Float`, `nil` | `nil` | Penalize based on presence |
|
|
80
|
-
| `frequency_penalty` | `Float`, `nil` | `nil` | Penalize based on frequency |
|
|
81
|
-
| `stop` | `String`, `Array`, `nil` | `nil` | Stop sequences |
|
|
97
|
+
| `temperature` | `Float`, `nil` | `nil` | Controls randomness — applied via `chat.with_temperature` |
|
|
98
|
+
| `top_p` | `Float`, `nil` | `nil` | Nucleus sampling threshold — applied via `chat.with_params` |
|
|
99
|
+
| `top_k` | `Integer`, `nil` | `nil` | Top-k sampling — applied via `chat.with_params` |
|
|
100
|
+
| `max_tokens` | `Integer`, `nil` | `nil` | Maximum tokens in response — applied via `chat.with_params` |
|
|
101
|
+
| `presence_penalty` | `Float`, `nil` | `nil` | Penalize based on presence — applied via `chat.with_params` |
|
|
102
|
+
| `frequency_penalty` | `Float`, `nil` | `nil` | Penalize based on frequency — applied via `chat.with_params` |
|
|
103
|
+
| `stop` | `String`, `Array`, `nil` | `nil` | Stop sequences — applied via `chat.with_params` |
|
|
82
104
|
|
|
83
105
|
When both `config:` and explicit kwargs (e.g., `temperature:`) are provided, explicit kwargs always win.
|
|
84
106
|
|
|
107
|
+
`model` and `temperature` are applied to the chat with dedicated `with_model` /
|
|
108
|
+
`with_temperature` calls. The remaining six LLM fields (`top_p`, `top_k`,
|
|
109
|
+
`max_tokens`, `presence_penalty`, `frequency_penalty`, `stop`) are collected into
|
|
110
|
+
a single `chat.with_params(...)` call. This distinction matters for template
|
|
111
|
+
front matter — see [Templates](#templates).
|
|
112
|
+
|
|
85
113
|
## Factory Method
|
|
86
114
|
|
|
87
115
|
```ruby
|
|
@@ -93,12 +121,15 @@ robot = RobotLab.build(
|
|
|
93
121
|
enable_cache: true,
|
|
94
122
|
bus: nil, # Optional TypedBus::MessageBus
|
|
95
123
|
skills: nil, # Optional skill templates
|
|
124
|
+
config: nil, # Optional RunConfig
|
|
96
125
|
**options # All other Robot.new parameters
|
|
97
126
|
)
|
|
98
127
|
# => RobotLab::Robot
|
|
99
128
|
```
|
|
100
129
|
|
|
101
|
-
If `name` is omitted, it defaults to `"robot"`.
|
|
130
|
+
If `name` is omitted, it defaults to the literal string `"robot"`. `**options`
|
|
131
|
+
is forwarded verbatim to `Robot.new`, whose keyword list is closed — an unknown
|
|
132
|
+
option raises `ArgumentError`.
|
|
102
133
|
|
|
103
134
|
## Attributes (Read-Only)
|
|
104
135
|
|
|
@@ -123,6 +154,7 @@ If `name` is omitted, it defaults to `"robot"`.
|
|
|
123
154
|
| `total_output_tokens` | `Integer` | Cumulative output tokens received across all `run()` calls |
|
|
124
155
|
| `learnings` | `Array<String>` | Accumulated cross-run observations (see [Learning Accumulation](#learning-accumulation)) |
|
|
125
156
|
| `budget_ledger` | `RobotLab::Budget::Ledger`, `nil` | Reserve/reconcile ledger backing `token_budget`/`cost_budget`; `nil` when neither is configured (see [Budgets](#budgets)) |
|
|
157
|
+
| `hooks` | `RobotLab::HookRegistry` | This robot's own hook registry. Populated by [`robot.on`](#on); consulted alongside `RobotLab.hooks` and the network's registry on every run |
|
|
126
158
|
|
|
127
159
|
## Attributes (Read-Write)
|
|
128
160
|
|
|
@@ -138,7 +170,9 @@ Used by tools like [`AskUser`](tool.md#built-in-askuser) that need terminal IO.
|
|
|
138
170
|
### run
|
|
139
171
|
|
|
140
172
|
```ruby
|
|
141
|
-
result = robot.run(message,
|
|
173
|
+
result = robot.run(message = nil, network: nil, task: nil,
|
|
174
|
+
network_memory: nil, network_config: nil, memory: nil,
|
|
175
|
+
mcp: :none, tools: :none, hooks: nil, **kwargs, &block)
|
|
142
176
|
# => RobotResult
|
|
143
177
|
```
|
|
144
178
|
|
|
@@ -148,35 +182,63 @@ Primary execution method. Sends a message to the LLM with memory/MCP/tools resol
|
|
|
148
182
|
|
|
149
183
|
| Name | Type | Default | Description |
|
|
150
184
|
|------|------|---------|-------------|
|
|
151
|
-
| `message` | `String` |
|
|
152
|
-
| `network` | `
|
|
153
|
-
| `
|
|
154
|
-
| `
|
|
185
|
+
| `message` | `String`, `nil` | `nil` | The user message to send (positional, optional) |
|
|
186
|
+
| `network` | `Network`, `nil` | `nil` | Network context (passed internally by `Network#run`) |
|
|
187
|
+
| `task` | `Task`, `nil` | `nil` | Task wrapper for the current pipeline step (passed internally); surfaces on hook contexts |
|
|
188
|
+
| `network_memory` | `Memory`, `nil` | `nil` | Shared network memory (passed internally) |
|
|
189
|
+
| `network_config` | `RunConfig`, `nil` | `nil` | Network-level config used when resolving `:inherit` for `mcp`/`tools` (passed internally) |
|
|
190
|
+
| `memory` | `Memory`, `Hash`, `nil` | `nil` | A `Memory` replaces the active memory for this run; a `Hash` is merged into it |
|
|
155
191
|
| `mcp` | `Symbol`, `Array` | `:none` | Runtime MCP override — `:inherit` (all attached servers), `:none`/`[]` (zero this turn), or an explicit array |
|
|
156
192
|
| `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) |
|
|
157
|
-
|
|
|
193
|
+
| `hooks` | `Array`, `nil` | `nil` | Per-run hook handler classes, active only for this call |
|
|
194
|
+
| `**kwargs` | `Hash` | `{}` | See below — **not** a passthrough to `Agent#ask` |
|
|
158
195
|
| `&block` | `Proc` | `nil` | Per-call streaming block, receives each content chunk |
|
|
159
196
|
|
|
197
|
+
**What `**kwargs` actually does.** Only `:with` is forwarded to the underlying
|
|
198
|
+
`Agent#ask` (`kwargs.slice(:with)`). *Every other* keyword is treated as
|
|
199
|
+
template re-render context: `kwargs.except(:with)` is merged over the build-time
|
|
200
|
+
context and the template is re-rendered before the call. If the robot has no
|
|
201
|
+
`template:`, those extra keywords are simply ignored.
|
|
202
|
+
|
|
203
|
+
```ruby
|
|
204
|
+
robot = RobotLab.build(name: "support", template: :support)
|
|
205
|
+
robot.run("Help me", company: "Acme") # re-renders the template with company: "Acme"
|
|
206
|
+
robot.run("Describe this", with: image) # forwarded to Agent#ask as attachments
|
|
207
|
+
```
|
|
208
|
+
|
|
160
209
|
When both a stored `on_content` callback and a runtime block are provided, both fire (stored first, then runtime block).
|
|
161
210
|
|
|
162
|
-
|
|
211
|
+
!!! warning "`tools:`/`mcp:` default to `:none` here"
|
|
212
|
+
A bare `robot.run(message)` sends **zero** tools and connects **no** MCP
|
|
213
|
+
servers for that call, even when `local_tools:`/`mcp:` were supplied at
|
|
214
|
+
build time. Pass `tools: :inherit` (and/or `mcp: :inherit`) explicitly to
|
|
215
|
+
use what is attached. `mcp: :inherit` triggers the connection attempt;
|
|
216
|
+
`tools: :inherit` is additionally required for the MCP tools to be sent.
|
|
217
|
+
|
|
218
|
+
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` (`DEFAULT_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
219
|
|
|
164
220
|
**Returns:** `RobotResult`
|
|
165
221
|
|
|
166
222
|
**Examples:**
|
|
167
223
|
|
|
168
224
|
```ruby
|
|
169
|
-
# Simple message
|
|
225
|
+
# Simple message — sends no tools, connects no MCP servers
|
|
170
226
|
result = robot.run("What is 2+2?")
|
|
171
227
|
|
|
228
|
+
# Send the tools attached via local_tools:
|
|
229
|
+
result = robot.run("What is 15 * 7?", tools: :inherit)
|
|
230
|
+
|
|
231
|
+
# Connect MCP servers and send their tools
|
|
232
|
+
result = robot.run("Search the repo", mcp: :inherit, tools: :inherit)
|
|
233
|
+
|
|
234
|
+
# Restrict this turn to a named subset
|
|
235
|
+
result = robot.run("Look it up", tools: %w[order_lookup])
|
|
236
|
+
|
|
172
237
|
# With runtime memory
|
|
173
238
|
result = robot.run("Summarize the data", memory: { data: report })
|
|
174
239
|
|
|
175
240
|
# With per-call streaming block
|
|
176
241
|
result = robot.run("Tell me a story") { |chunk| print chunk.content }
|
|
177
|
-
|
|
178
|
-
# With runtime overrides
|
|
179
|
-
result = robot.run("Help me", mcp: :none, tools: :none)
|
|
180
242
|
```
|
|
181
243
|
|
|
182
244
|
### model
|
|
@@ -187,6 +249,21 @@ robot.model # => "claude-sonnet-4" or nil
|
|
|
187
249
|
|
|
188
250
|
Returns the model ID string. Resolves through the underlying chat object.
|
|
189
251
|
|
|
252
|
+
### effective_config
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
robot.effective_config
|
|
256
|
+
# => { model: "claude-sonnet-4-20250514", temperature: 0.7, max_tokens: 4096 }
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Snapshot of the robot's merged `RunConfig` as a plain Hash, `.compact`ed so unset
|
|
260
|
+
fields are omitted. Reports exactly these keys when set: `model`, `temperature`,
|
|
261
|
+
`top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop`,
|
|
262
|
+
`tools`, `mcp`, `max_tool_rounds`, `doom_loop_threshold`, `auto_compact`,
|
|
263
|
+
`compact_threshold`, `token_budget`, `cost_budget`.
|
|
264
|
+
|
|
265
|
+
This is a *view*, not the config object — use `robot.config` for the `RunConfig` itself.
|
|
266
|
+
|
|
190
267
|
### update
|
|
191
268
|
|
|
192
269
|
```ruby
|
|
@@ -203,29 +280,52 @@ robot.update(
|
|
|
203
280
|
|
|
204
281
|
Reconfigure the robot after construction. Returns `self` for chaining.
|
|
205
282
|
|
|
283
|
+
The five named parameters are applied directly (`template` re-renders the prompt;
|
|
284
|
+
`system_prompt`, `model`, and `temperature` call the corresponding `with_*` on the chat).
|
|
285
|
+
|
|
286
|
+
!!! warning "`**kwargs` only reaches fields the chat exposes as `with_<key>`"
|
|
287
|
+
Each extra keyword is forwarded as `@chat.with_#{key}(value)` **only if
|
|
288
|
+
`@chat.respond_to?(:"with_#{key}")`**. `RubyLLM::Chat` has no
|
|
289
|
+
`with_max_tokens`, `with_top_p`, `with_top_k`, `with_stop`,
|
|
290
|
+
`with_presence_penalty`, or `with_frequency_penalty` — so
|
|
291
|
+
`robot.update(max_tokens: 4000)` silently does nothing. Use
|
|
292
|
+
`robot.with_params(max_tokens: 4000)` for those fields.
|
|
293
|
+
|
|
206
294
|
### with_* Methods (Chaining)
|
|
207
295
|
|
|
208
|
-
|
|
296
|
+
`with_*` methods are discovered from `RubyLLM::Chat` at construction time and
|
|
297
|
+
defined as singleton methods that delegate to the persistent `@chat` and return
|
|
298
|
+
`self` for chaining. This is the **complete** set:
|
|
209
299
|
|
|
210
300
|
| Method | Description |
|
|
211
301
|
|--------|-------------|
|
|
212
302
|
| `with_model(model_id)` | Change the LLM model |
|
|
213
303
|
| `with_temperature(temp)` | Set temperature |
|
|
214
|
-
| `with_top_p(value)` | Set nucleus sampling |
|
|
215
|
-
| `with_top_k(value)` | Set top-k sampling |
|
|
216
|
-
| `with_max_tokens(value)` | Set max response tokens |
|
|
217
|
-
| `with_presence_penalty(value)` | Set presence penalty |
|
|
218
|
-
| `with_frequency_penalty(value)` | Set frequency penalty |
|
|
219
|
-
| `with_stop(sequences)` | Set stop sequences |
|
|
220
304
|
| `with_instructions(prompt)` | Set system instructions |
|
|
221
305
|
| `with_tool(tool)` | Add a single tool |
|
|
222
306
|
| `with_tools(*tools)` | Add multiple tools |
|
|
223
|
-
| `with_params(**params)` | Set
|
|
307
|
+
| `with_params(**params)` | Set arbitrary provider parameters |
|
|
224
308
|
| `with_headers(**headers)` | Set custom headers |
|
|
225
309
|
| `with_schema(schema)` | Set output schema |
|
|
226
310
|
| `with_context(**ctx)` | Set context |
|
|
227
311
|
| `with_thinking(opts)` | Enable extended thinking |
|
|
228
|
-
|
|
312
|
+
|
|
313
|
+
Plus two defined by RobotLab itself:
|
|
314
|
+
|
|
315
|
+
| Method | Description |
|
|
316
|
+
|--------|-------------|
|
|
317
|
+
| `with_template(id, **context)` | Apply a prompt_manager template (see below) |
|
|
318
|
+
| `with_bus(bus = nil)` | Connect to a message bus (creates one if nil) |
|
|
319
|
+
|
|
320
|
+
!!! danger "These do not exist"
|
|
321
|
+
`with_max_tokens`, `with_top_p`, `with_top_k`, `with_stop`,
|
|
322
|
+
`with_presence_penalty`, and `with_frequency_penalty` are **not** defined
|
|
323
|
+
and raise `NoMethodError`. Set those fields with a constructor kwarg
|
|
324
|
+
(`max_tokens: 2000`) or with `with_params`:
|
|
325
|
+
|
|
326
|
+
```ruby
|
|
327
|
+
robot.with_params(max_tokens: 2000, top_p: 0.3)
|
|
328
|
+
```
|
|
229
329
|
|
|
230
330
|
**Example:**
|
|
231
331
|
|
|
@@ -234,6 +334,7 @@ robot = RobotLab.build(name: "bot")
|
|
|
234
334
|
robot
|
|
235
335
|
.with_model("claude-sonnet-4")
|
|
236
336
|
.with_temperature(0.7)
|
|
337
|
+
.with_params(max_tokens: 2000)
|
|
237
338
|
.with_instructions("Be concise.")
|
|
238
339
|
.run("Hello")
|
|
239
340
|
```
|
|
@@ -276,7 +377,7 @@ message = robot.send_message(to: :bob, content: "Tell me a joke.")
|
|
|
276
377
|
# => RobotMessage
|
|
277
378
|
```
|
|
278
379
|
|
|
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
|
|
380
|
+
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 never clobber each other.
|
|
280
381
|
|
|
281
382
|
**Parameters:**
|
|
282
383
|
|
|
@@ -360,7 +461,7 @@ Auto-answer inbound (non-reply) bus tasks: run the block to produce a reply, and
|
|
|
360
461
|
|
|
361
462
|
**Returns:** `self`
|
|
362
463
|
|
|
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
|
|
464
|
+
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 **inline in the caller's context** — `BusPoller` has no background thread; its `enqueue` either processes the delivery immediately or queues it behind the one in flight and drains it when that finishes. Deliveries to a given robot are therefore handled one at a time, and a long-running responder blocks the sender as well as the next inbound message.
|
|
364
465
|
|
|
365
466
|
```ruby
|
|
366
467
|
bob.respond_to_tasks { |message| "handled: #{message.content}" }
|
|
@@ -431,6 +532,69 @@ worker2 = bot.spawn(name: "worker", system_prompt: "Worker 2")
|
|
|
431
532
|
# Messages sent to :worker are delivered to both
|
|
432
533
|
```
|
|
433
534
|
|
|
535
|
+
### assign_bus_poller
|
|
536
|
+
|
|
537
|
+
```ruby
|
|
538
|
+
robot.assign_bus_poller(poller, group: :default)
|
|
539
|
+
# => void — do not rely on the return value
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
Adopt a shared [`BusPoller`](../support.md#robotlabbuspoller) — normally the
|
|
543
|
+
network's. `Network#task` calls this for every robot that responds to it, passing
|
|
544
|
+
the task's `poller_group:`. Any private poller the robot auto-created is dropped
|
|
545
|
+
first.
|
|
546
|
+
|
|
547
|
+
**Parameters:**
|
|
548
|
+
|
|
549
|
+
| Name | Type | Default | Description |
|
|
550
|
+
|------|------|---------|-------------|
|
|
551
|
+
| `poller` | `BusPoller` | **required** | The shared poller to adopt |
|
|
552
|
+
| `group` | `Symbol` | `:default` | Poller group label — informational only; groups share one drain mechanism |
|
|
553
|
+
|
|
554
|
+
You only call this directly when wiring robots onto a shared poller outside a
|
|
555
|
+
`Network`.
|
|
556
|
+
|
|
557
|
+
### inherited_llm_settings
|
|
558
|
+
|
|
559
|
+
```ruby
|
|
560
|
+
robot.inherited_llm_settings
|
|
561
|
+
# => { model: "llama3.2", provider: :ollama }
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
The model/provider pair a [`spawn`](#spawn)ed child inherits from this robot.
|
|
565
|
+
Returns `{}` when neither is set — each key is included only when the
|
|
566
|
+
corresponding reader is truthy. Exposed so an application building children by
|
|
567
|
+
some route other than `spawn` can apply the same inheritance:
|
|
568
|
+
|
|
569
|
+
```ruby
|
|
570
|
+
child = RobotLab.build(name: "helper", bus: parent.bus, **parent.inherited_llm_settings)
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
### rerender_template
|
|
574
|
+
|
|
575
|
+
```ruby
|
|
576
|
+
robot.rerender_template(run_context) # internal — see the warning below
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
Re-renders the robot's template with `run_context` merged over the build-time
|
|
580
|
+
context (skill bodies included, when `skills:` are in play) and reinstalls the
|
|
581
|
+
result as the system prompt, re-appending the inline `system_prompt`. `run` calls
|
|
582
|
+
it automatically when the robot has a `template:` and the call carried extra
|
|
583
|
+
keywords — every keyword except `:with` — which is the mechanism behind
|
|
584
|
+
`robot.run("Help me", company: "Acme")`.
|
|
585
|
+
|
|
586
|
+
`Robot::AgentSkillMatching` overrides it to re-prepend any matched AgentSkill
|
|
587
|
+
instructions afterward, because a re-render replaces the whole system prompt and
|
|
588
|
+
would otherwise discard them mid-run.
|
|
589
|
+
|
|
590
|
+
!!! warning "Public only by accident — treat it as internal"
|
|
591
|
+
`rerender_template` is `private` in `Robot::TemplateRendering`, but the
|
|
592
|
+
prepended `Robot::AgentSkillMatching` redefines it **above** its own
|
|
593
|
+
`private` keyword, so the effective method on `Robot` is public. That is an
|
|
594
|
+
artifact of the override, not a supported entry point: the return value is
|
|
595
|
+
unspecified, and it mutates the chat's system prompt for the rest of the
|
|
596
|
+
conversation. Pass template context to `run` instead.
|
|
597
|
+
|
|
434
598
|
### with_bus
|
|
435
599
|
|
|
436
600
|
```ruby
|
|
@@ -506,7 +670,18 @@ Inject pre-connected MCP clients and their tools into this robot. Used by host a
|
|
|
506
670
|
```ruby
|
|
507
671
|
# Host app manages MCP connections
|
|
508
672
|
clients = { "github" => github_client }
|
|
509
|
-
|
|
673
|
+
|
|
674
|
+
# There is no Tool.from_mcp — MCP wrappers are built with Tool.create,
|
|
675
|
+
# exactly as RobotLab's own discover_mcp_tools does.
|
|
676
|
+
tools = github_client.list_tools.map do |tool_def|
|
|
677
|
+
name = tool_def[:name]
|
|
678
|
+
RobotLab::Tool.create(
|
|
679
|
+
name: name,
|
|
680
|
+
description: tool_def[:description],
|
|
681
|
+
parameters: tool_def[:inputSchema],
|
|
682
|
+
mcp: "github"
|
|
683
|
+
) { |args| github_client.call_tool(name, args) }
|
|
684
|
+
end
|
|
510
685
|
|
|
511
686
|
robot.inject_mcp!(clients: clients, tools: tools)
|
|
512
687
|
```
|
|
@@ -573,6 +748,147 @@ saved = robot.messages.dup
|
|
|
573
748
|
robot.replace_messages(saved)
|
|
574
749
|
```
|
|
575
750
|
|
|
751
|
+
### compress_history
|
|
752
|
+
|
|
753
|
+
```ruby
|
|
754
|
+
robot.compress_history(
|
|
755
|
+
recent_turns: 3,
|
|
756
|
+
keep_threshold: 0.6,
|
|
757
|
+
drop_threshold: 0.2,
|
|
758
|
+
summarizer: nil
|
|
759
|
+
)
|
|
760
|
+
# => self
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
Shrink the conversation by scoring each older turn against the most recent
|
|
764
|
+
context and dropping or summarizing the least relevant ones. Internally builds a
|
|
765
|
+
`RobotLab::HistoryCompressor` and hands the result to `replace_messages`.
|
|
766
|
+
|
|
767
|
+
**Parameters:**
|
|
768
|
+
|
|
769
|
+
| Name | Type | Default | Description |
|
|
770
|
+
|------|------|---------|-------------|
|
|
771
|
+
| `recent_turns` | `Integer` | `3` | Turn pairs at the end that are always kept verbatim |
|
|
772
|
+
| `keep_threshold` | `Float` | `0.6` | Cosine score at or above this → kept verbatim |
|
|
773
|
+
| `drop_threshold` | `Float` | `0.2` | Cosine score below this → dropped |
|
|
774
|
+
| `summarizer` | `#call`, `nil` | `nil` | `callable(text) -> String` applied to the medium tier; `nil` drops the medium tier instead |
|
|
775
|
+
|
|
776
|
+
**Returns:** `self`
|
|
777
|
+
|
|
778
|
+
System messages and tool-call/tool-result messages are always preserved.
|
|
779
|
+
|
|
780
|
+
Scoring uses **term-frequency cosine similarity without IDF** (see
|
|
781
|
+
`RobotLab::Convergence`), so it is a lexical overlap measure, not a semantic one.
|
|
782
|
+
|
|
783
|
+
**Raises:** `RobotLab::DependencyError` when the optional `classifier` gem
|
|
784
|
+
(`~> 2.3`) is not installed.
|
|
785
|
+
|
|
786
|
+
```ruby
|
|
787
|
+
robot.compress_history(recent_turns: 5, summarizer: ->(text) { text[0, 200] })
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
`auto_compact: :context_window` on a `RunConfig` calls this automatically before
|
|
791
|
+
an LLM call once estimated tokens exceed `compact_threshold` (default `0.80`) of
|
|
792
|
+
the model's context window. When the `classifier` gem is missing there, the
|
|
793
|
+
`DependencyError` is caught, logged at `:warn`, and compaction is skipped.
|
|
794
|
+
|
|
795
|
+
### delegate
|
|
796
|
+
|
|
797
|
+
```ruby
|
|
798
|
+
result = robot.delegate(to:, task:, async: false, **run_kwargs)
|
|
799
|
+
# => RobotResult (async: false) | DelegationFuture (async: true)
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
Hand a task to another robot and annotate the result with delegation metadata.
|
|
803
|
+
|
|
804
|
+
**Parameters:**
|
|
805
|
+
|
|
806
|
+
| Name | Type | Default | Description |
|
|
807
|
+
|------|------|---------|-------------|
|
|
808
|
+
| `to` | `Robot` | **required** | The robot to delegate to |
|
|
809
|
+
| `task` | `String` | **required** | The message to send |
|
|
810
|
+
| `async` | `Boolean` | `false` | When true, returns a `DelegationFuture` immediately |
|
|
811
|
+
| `**run_kwargs` | `Hash` | `{}` | Forwarded verbatim to the delegatee's `run` — including `tools:`/`mcp:`, which still default to `:none` |
|
|
812
|
+
|
|
813
|
+
**Synchronous** (default) blocks until the delegatee finishes and returns its
|
|
814
|
+
`RobotResult` with `duration` and `delegated_by` set.
|
|
815
|
+
|
|
816
|
+
**Asynchronous** (`async: true`) runs the delegatee on a new `Thread` and returns
|
|
817
|
+
a `RobotLab::DelegationFuture`. Call `future.value` to block, `future.value(timeout: N)`
|
|
818
|
+
to block with a deadline (raises `RobotLab::DelegationFuture::DelegationTimeout`),
|
|
819
|
+
or `future.resolved?` to poll. An exception in the delegatee is captured and
|
|
820
|
+
re-raised from `future.value`.
|
|
821
|
+
|
|
822
|
+
```ruby
|
|
823
|
+
# Synchronous
|
|
824
|
+
result = manager.delegate(to: analyst, task: "What are the risks?")
|
|
825
|
+
result.reply
|
|
826
|
+
result.delegated_by # => "manager"
|
|
827
|
+
result.duration # => 1.43
|
|
828
|
+
|
|
829
|
+
# Async fan-out
|
|
830
|
+
f1 = manager.delegate(to: summarizer, task: "summarize ...", async: true)
|
|
831
|
+
f2 = manager.delegate(to: analyst, task: "analyze ...", async: true, tools: :inherit)
|
|
832
|
+
summary = f1.value
|
|
833
|
+
analysis = f2.value(timeout: 30)
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
### search_history
|
|
837
|
+
|
|
838
|
+
```ruby
|
|
839
|
+
results = robot.search_history(query, limit: 5)
|
|
840
|
+
# => Array<RobotLab::Robot::HistorySearch::HistoryResult>
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
Rank the robot's own conversation messages against a natural-language query
|
|
844
|
+
using stemmed term-frequency cosine similarity.
|
|
845
|
+
|
|
846
|
+
**Parameters:**
|
|
847
|
+
|
|
848
|
+
| Name | Type | Default | Description |
|
|
849
|
+
|------|------|---------|-------------|
|
|
850
|
+
| `query` | `String` | **required** | Natural-language search query |
|
|
851
|
+
| `limit` | `Integer` | `5` | Maximum results to return |
|
|
852
|
+
|
|
853
|
+
**Returns:** `Array<HistoryResult>` sorted by score descending. `HistoryResult`
|
|
854
|
+
is a `Data` type with members `text`, `role`, `score`, and `index`.
|
|
855
|
+
|
|
856
|
+
Messages shorter than `MIN_SCORE_LENGTH` (20 characters) are skipped, as are
|
|
857
|
+
messages that score zero.
|
|
858
|
+
|
|
859
|
+
**Raises:** `RobotLab::DependencyError` when the optional `classifier` gem is not installed.
|
|
860
|
+
|
|
861
|
+
```ruby
|
|
862
|
+
robot.search_history("quarterly revenue", limit: 3).each do |r|
|
|
863
|
+
puts "[#{r.role}] (#{r.score.round(3)}) #{r.text}"
|
|
864
|
+
end
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
### on
|
|
868
|
+
|
|
869
|
+
```ruby
|
|
870
|
+
robot.on(HandlerClass, context: nil)
|
|
871
|
+
# => the registration
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
Register a hook handler on **this robot's** registry (`robot.hooks`). The robot's
|
|
875
|
+
registry is consulted on every run alongside `RobotLab.hooks` (global) and the
|
|
876
|
+
network's registry, in that order.
|
|
877
|
+
|
|
878
|
+
**Parameters:**
|
|
879
|
+
|
|
880
|
+
| Name | Type | Default | Description |
|
|
881
|
+
|------|------|---------|-------------|
|
|
882
|
+
| `handler_class` | `Class` | **required** | The hook handler class |
|
|
883
|
+
| `context` | `Object`, `nil` | `nil` | Optional per-registration context passed to the handler |
|
|
884
|
+
|
|
885
|
+
!!! note "Task hooks bypass robot registries"
|
|
886
|
+
The `:task` hook family resolves against `[RobotLab.hooks, network&.hooks]`
|
|
887
|
+
only. A handler registered with `robot.on` never fires for task hooks —
|
|
888
|
+
register it with `RobotLab.on` or `network.on` instead.
|
|
889
|
+
|
|
890
|
+
Handlers can also be scoped to a single call with `robot.run(msg, hooks: [HandlerClass])`.
|
|
891
|
+
|
|
576
892
|
### chat_provider
|
|
577
893
|
|
|
578
894
|
```ruby
|
|
@@ -615,7 +931,23 @@ robot.to_h
|
|
|
615
931
|
# => Hash
|
|
616
932
|
```
|
|
617
933
|
|
|
618
|
-
Returns a hash representation of the robot
|
|
934
|
+
Returns a hash representation of the robot. Keys, in order: `name`,
|
|
935
|
+
`description`, `template`, `skills`, `system_prompt`, `local_tools` (tool names),
|
|
936
|
+
`mcp_tools` (tool names), `mcp_config`, `tools_config`, `mcp_servers` (connected
|
|
937
|
+
client names), `model`, `config` (the `RunConfig` as a JSON-safe hash, omitted
|
|
938
|
+
when the config is empty), and `bus` (`true` if configured, omitted otherwise).
|
|
939
|
+
The whole hash is `.compact`ed, so nil values are dropped.
|
|
940
|
+
|
|
941
|
+
```ruby
|
|
942
|
+
RobotLab.build(name: "x", max_tokens: 100).to_h
|
|
943
|
+
# => { name: "x", local_tools: [], mcp_tools: [], mcp_config: :none,
|
|
944
|
+
# tools_config: :none, mcp_servers: [], model: "claude-sonnet-4-20250514",
|
|
945
|
+
# config: { max_tokens: 100, enable_cache: true } }
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
The `config` value comes from `RunConfig#to_json_hash`, which omits the
|
|
949
|
+
non-serializable fields (`on_tool_call`, `on_tool_result`, `on_content`, `bus`,
|
|
950
|
+
`auto_compact`).
|
|
619
951
|
|
|
620
952
|
## Memory Behavior
|
|
621
953
|
|
|
@@ -640,20 +972,60 @@ Templates are `.md` files with optional YAML front matter, loaded via `prompt_ma
|
|
|
640
972
|
robot = RobotLab.build(name: "bot", template: :assistant, context: { tone: "friendly" })
|
|
641
973
|
```
|
|
642
974
|
|
|
643
|
-
Front matter supports two categories of keys
|
|
975
|
+
Front matter supports two categories of keys.
|
|
976
|
+
|
|
977
|
+
**LLM Config:** `model`, `temperature`, `top_p`, `top_k`, `max_tokens`,
|
|
978
|
+
`presence_penalty`, `frequency_penalty`, `stop` are all *parsed* into a
|
|
979
|
+
`RunConfig`.
|
|
644
980
|
|
|
645
|
-
|
|
981
|
+
!!! warning "Only `model` and `temperature` actually take effect from front matter"
|
|
982
|
+
Front-matter LLM fields are applied through `RunConfig#apply_to`, which
|
|
983
|
+
dispatches `chat.with_<field>` guarded by `respond_to?`. `RubyLLM::Chat`
|
|
984
|
+
defines only `with_model` and `with_temperature`, so `top_p`, `top_k`,
|
|
985
|
+
`max_tokens`, `presence_penalty`, `frequency_penalty`, and `stop` are parsed
|
|
986
|
+
and **silently dropped**. Set those six as constructor kwargs or on a
|
|
987
|
+
`config:` `RunConfig` instead — that path goes through `with_params` and
|
|
988
|
+
does work.
|
|
646
989
|
|
|
647
990
|
**Robot Extras:** `robot_name`, `description`, `tools`, `mcp`, `skills` — applied to the robot's identity and capabilities. Constructor-provided values always take precedence.
|
|
648
991
|
|
|
649
992
|
| Key | Type | Description |
|
|
650
993
|
|-----|------|-------------|
|
|
651
|
-
| `robot_name` | `String` | Override robot name
|
|
652
|
-
| `description` | `String` | Human-readable description |
|
|
653
|
-
| `tools` | `Array
|
|
654
|
-
| `mcp` | `Array<Hash>` | MCP server configurations |
|
|
994
|
+
| `robot_name` | `String` | Override robot name — applied only when the constructor name is still the default `"robot"` |
|
|
995
|
+
| `description` | `String` | Human-readable description; applied only when the constructor passed no `description:` |
|
|
996
|
+
| `tools` | `Array` | Tool entries; applied only when `local_tools:` is empty (see below) |
|
|
997
|
+
| `mcp` | `Array<Hash>` | MCP server configurations; applied only when the constructor `mcp:` is `:none` |
|
|
655
998
|
| `skills` | `Array<Symbol>` | Skill templates to prepend (recursive, with cycle detection) |
|
|
656
999
|
|
|
1000
|
+
Templates render with ERB — write `<%= var %>`. `{{ var }}` is not interpolated
|
|
1001
|
+
and passes through verbatim.
|
|
1002
|
+
|
|
1003
|
+
### Front-matter `tools:` resolution
|
|
1004
|
+
|
|
1005
|
+
Front-matter `tools` entries are resolved by `resolve_frontmatter_tools`, and the
|
|
1006
|
+
result becomes `local_tools` (real tool objects), **not** the `tools_config`
|
|
1007
|
+
name allowlist. Three entry shapes are accepted:
|
|
1008
|
+
|
|
1009
|
+
| Entry | Behavior |
|
|
1010
|
+
|-------|----------|
|
|
1011
|
+
| `String` | Resolved with `Object.const_get`. If the constant is a `Class` that is `< RubyLLM::Tool`, it is **instantiated** (`const.new`); any other constant is used as-is |
|
|
1012
|
+
| `Class` | **Instantiated** (`name.new`) |
|
|
1013
|
+
| anything else | Used as-is (e.g. an already-built tool instance) |
|
|
1014
|
+
|
|
1015
|
+
An unresolvable name does **not** raise. It is logged at `:warn`
|
|
1016
|
+
(`"Robot '<name>': tool '<X>' not found, skipping"`) and skipped.
|
|
1017
|
+
|
|
1018
|
+
```markdown
|
|
1019
|
+
---
|
|
1020
|
+
tools:
|
|
1021
|
+
- OrderLookup # instantiated: OrderLookup.new
|
|
1022
|
+
- RefundProcessor
|
|
1023
|
+
---
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
Because `run()` still defaults to `tools: :none`, front-matter tools are sent
|
|
1027
|
+
only when you pass `tools: :inherit` at run time.
|
|
1028
|
+
|
|
657
1029
|
## Skills
|
|
658
1030
|
|
|
659
1031
|
Skills compose robot behaviors from reusable templates. Each skill is a standard `.md` template whose prompt body is prepended before the main template. Skills are expanded depth-first with automatic cycle detection.
|
|
@@ -704,7 +1076,45 @@ robot = RobotLab.build(
|
|
|
704
1076
|
robot.config #=> RunConfig with model: "claude-sonnet-4", temperature: 0.9, ...
|
|
705
1077
|
```
|
|
706
1078
|
|
|
707
|
-
RunConfig
|
|
1079
|
+
`RunConfig::FIELDS` is the complete, authoritative list. Passing any other key to
|
|
1080
|
+
`RunConfig.new` raises `ArgumentError: Unknown RunConfig field: ...`.
|
|
1081
|
+
|
|
1082
|
+
| Group | Constant | Fields |
|
|
1083
|
+
|-------|----------|--------|
|
|
1084
|
+
| LLM | `LLM_FIELDS` | `model`, `temperature`, `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop` |
|
|
1085
|
+
| Tools | `TOOL_FIELDS` | `mcp`, `tools` |
|
|
1086
|
+
| Callbacks | `CALLBACK_FIELDS` | `on_tool_call`, `on_tool_result`, `on_content` |
|
|
1087
|
+
| Infrastructure | `INFRA_FIELDS` | `bus`, `enable_cache`, `max_tool_rounds`, `token_budget`, `cost_budget`, `ractor_pool_size`, `max_concurrent_robots`, `doom_loop_threshold`, `auto_compact`, `compact_threshold`, `max_tools` |
|
|
1088
|
+
|
|
1089
|
+
Five of those infrastructure fields are `RunConfig`-only — they are **not**
|
|
1090
|
+
`Robot.new` keywords: `ractor_pool_size`, `max_concurrent_robots`,
|
|
1091
|
+
`auto_compact`, `compact_threshold`, `max_tools`. (`max_concurrent_robots` is
|
|
1092
|
+
consumed by `Network`, not by `Robot`; `ractor_pool_size` by the
|
|
1093
|
+
`robot_lab-ractor` extension.)
|
|
1094
|
+
|
|
1095
|
+
```ruby
|
|
1096
|
+
config = RobotLab::RunConfig.new(auto_compact: :context_window, compact_threshold: 0.7, max_tools: 32)
|
|
1097
|
+
robot = RobotLab.build(name: "long_runner", system_prompt: "...", config: config)
|
|
1098
|
+
```
|
|
1099
|
+
|
|
1100
|
+
Other `RunConfig` API:
|
|
1101
|
+
|
|
1102
|
+
| Method | Description |
|
|
1103
|
+
|--------|-------------|
|
|
1104
|
+
| `RunConfig.new(**kwargs) { \|c\| ... }` | Keyword construction plus an optional block DSL (`c.model "..."`) |
|
|
1105
|
+
| `#merge(other)` | Returns a **new** RunConfig; the other's non-nil values win |
|
|
1106
|
+
| `#to_h` | The explicitly-set fields |
|
|
1107
|
+
| `#to_json_hash` | `to_h` minus `NON_SERIALIZABLE_FIELDS` (`on_tool_call`, `on_tool_result`, `on_content`, `bus`, `auto_compact`) |
|
|
1108
|
+
| `#apply_to(chat, provider: nil, assume_model_exists: false)` | Applies `LLM_FIELDS` via `chat.with_<field>`, guarded by `respond_to?` |
|
|
1109
|
+
| `#empty?` / `#key?(field)` | Introspection |
|
|
1110
|
+
| `RunConfig.from_front_matter(metadata)` | Builds a RunConfig from a template's parsed metadata |
|
|
1111
|
+
|
|
1112
|
+
!!! note "A network-level `config:` only propagates `mcp` and `tools`"
|
|
1113
|
+
LLM fields and callbacks (`on_content`, `on_tool_call`, `on_tool_result`)
|
|
1114
|
+
are read from the robot's own config at construction time and are never
|
|
1115
|
+
inherited from a network. A member robot picks up the network's `mcp`/`tools`
|
|
1116
|
+
only when it opts in with `:inherit`. `max_concurrent_robots` is the one
|
|
1117
|
+
field the network itself consumes.
|
|
708
1118
|
|
|
709
1119
|
See [Configuration: RunConfig](../../getting-started/configuration.md#runconfig-shared-operational-defaults) for full details.
|
|
710
1120
|
|
|
@@ -778,27 +1188,55 @@ robot.run("Tell me a story") { |chunk| stream_to_client(chunk.content) }
|
|
|
778
1188
|
|
|
779
1189
|
## Configuration Hierarchy
|
|
780
1190
|
|
|
781
|
-
Tools and MCP servers use hierarchical resolution: **runtime > robot > network > global config**.
|
|
1191
|
+
Tools and MCP servers use hierarchical resolution: **runtime > robot > task > network > global config**.
|
|
782
1192
|
|
|
783
1193
|
```
|
|
784
1194
|
RobotLab.config (global)
|
|
785
1195
|
|
|
|
786
|
-
+-- Network (config:)
|
|
1196
|
+
+-- Network (config:) -- propagates only mcp/tools to members
|
|
787
1197
|
| |
|
|
788
|
-
| +-- Task (config:)
|
|
1198
|
+
| +-- Task (config:) -- likewise only mcp/tools
|
|
789
1199
|
| | |
|
|
790
1200
|
| | +-- Robot (config: + build-time mcp:, tools:)
|
|
791
1201
|
| | |
|
|
792
|
-
| | +--
|
|
793
|
-
| | |
|
|
794
|
-
| | +-- run() call (runtime mcp:, tools:)
|
|
1202
|
+
| | +-- run() call (runtime mcp:, tools:) <- default :none
|
|
795
1203
|
```
|
|
796
1204
|
|
|
797
1205
|
Values at each level:
|
|
798
1206
|
|
|
799
|
-
- `:none` -- no tools/MCP at this level
|
|
800
|
-
- `:inherit` -- inherit from parent level
|
|
801
|
-
- `Array` --
|
|
1207
|
+
- `:none` -- no tools/MCP at this level (the default at every level)
|
|
1208
|
+
- `:inherit` -- inherit from the parent level
|
|
1209
|
+
- `Array` -- a filter over the already-attached tools, or a list of MCP server configs. Entries are matched against `tool.name.to_s`, so they must be written in the same form the tool was attached in: a class-attached tool matches `"RefundTool"`, an instance-attached one matches `"refund"`. (The constructor's `tools:` accepts only Strings/Symbols; the class form is usable at the task/`run` level, which is not validated.)
|
|
1210
|
+
|
|
1211
|
+
!!! danger "For a standalone robot, do not set `tools: :inherit` at build time"
|
|
1212
|
+
The parent is recomputed on every run as
|
|
1213
|
+
`network_config&.tools || network_parent_config(network)&.tools || RobotLab.config.tools`.
|
|
1214
|
+
For a **standalone** robot that resolves to the global `:none`, so a
|
|
1215
|
+
build-time `:inherit` produces the allowlist `["none"]`, which matches
|
|
1216
|
+
nothing. Leave `tools:` unset on the constructor and pass `tools: :inherit`
|
|
1217
|
+
on `run()` instead.
|
|
1218
|
+
|
|
1219
|
+
| build `tools:` | run `tools:` | tools sent |
|
|
1220
|
+
|---|---|---|
|
|
1221
|
+
| unset | `:none` (default) | none |
|
|
1222
|
+
| unset | `:inherit` | all attached — **the correct pattern** |
|
|
1223
|
+
| `:inherit` | `:inherit` | none — broken |
|
|
1224
|
+
| `:none` | `:inherit` | all attached |
|
|
1225
|
+
|
|
1226
|
+
This does **not** generalize to robots inside a network. When the network's
|
|
1227
|
+
`config:` sets `tools:`/`mcp:`, the parent resolved at run time is that
|
|
1228
|
+
network value, and a build-time `:inherit` is exactly how the robot opts
|
|
1229
|
+
into it. See [MCP in Networks](../mcp/index.md#mcp-in-networks).
|
|
1230
|
+
|
|
1231
|
+
### Per-robot config cascade
|
|
1232
|
+
|
|
1233
|
+
For a single robot, least- to most-specific:
|
|
1234
|
+
|
|
1235
|
+
```
|
|
1236
|
+
template front matter -> config: (RunConfig) -> constructor kwargs
|
|
1237
|
+
```
|
|
1238
|
+
|
|
1239
|
+
Front matter is the **base**, not an override. Constructor kwargs always win.
|
|
802
1240
|
|
|
803
1241
|
## Examples
|
|
804
1242
|
|
|
@@ -848,9 +1286,14 @@ robot = RobotLab.build(
|
|
|
848
1286
|
system_prompt: "You help with math.",
|
|
849
1287
|
local_tools: [Calculator]
|
|
850
1288
|
)
|
|
851
|
-
|
|
1289
|
+
|
|
1290
|
+
# run() defaults to tools: :none — pass :inherit to actually send Calculator
|
|
1291
|
+
result = robot.run("What is 15 * 7?", tools: :inherit)
|
|
852
1292
|
```
|
|
853
1293
|
|
|
1294
|
+
Note that `param` accepts only `type:`, `desc:`/`description:`, and `required:` —
|
|
1295
|
+
there is no `enum:` option. See [Tool](tool.md#param).
|
|
1296
|
+
|
|
854
1297
|
### Robot with Local Provider
|
|
855
1298
|
|
|
856
1299
|
```ruby
|
|
@@ -880,10 +1323,15 @@ robot = RobotLab.build(
|
|
|
880
1323
|
}
|
|
881
1324
|
]
|
|
882
1325
|
)
|
|
883
|
-
|
|
1326
|
+
|
|
1327
|
+
# mcp: :inherit triggers the connection; tools: :inherit sends the discovered tools
|
|
1328
|
+
result = robot.run("Search for popular Ruby repos", mcp: :inherit, tools: :inherit)
|
|
884
1329
|
robot.disconnect
|
|
885
1330
|
```
|
|
886
1331
|
|
|
1332
|
+
`transport:` must be a nested hash. MCP connection failures are logged and
|
|
1333
|
+
recorded in `robot.failed_mcp_server_names` — they are not raised.
|
|
1334
|
+
|
|
887
1335
|
### Robot with Skills
|
|
888
1336
|
|
|
889
1337
|
```ruby
|
|
@@ -985,7 +1433,7 @@ Every `robot.run()` returns a `RobotResult` with token counts for that call. The
|
|
|
985
1433
|
|
|
986
1434
|
```ruby
|
|
987
1435
|
robot.reset_token_totals
|
|
988
|
-
# =>
|
|
1436
|
+
# => the robot itself (returns self, so it chains)
|
|
989
1437
|
```
|
|
990
1438
|
|
|
991
1439
|
Reset the cumulative accounting counters to zero. Useful when you want to measure cost for a specific task batch while keeping the robot alive for the next batch.
|
|
@@ -1030,7 +1478,7 @@ Each `run()` reserves the remaining budget for every configured dimension before
|
|
|
1030
1478
|
- **`RobotLab::BudgetExceeded`** — raised up front when a *prior* call already exhausted a dimension; the new call is refused before it spends anything.
|
|
1031
1479
|
- **`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
1480
|
|
|
1033
|
-
See [Budgets](../../guides/observability.md#budgets-token
|
|
1481
|
+
See [Budgets](../../guides/observability.md#budgets-token-cost) for the full walkthrough.
|
|
1034
1482
|
|
|
1035
1483
|
## Tool Loop Circuit Breaker
|
|
1036
1484
|
|
|
@@ -1058,7 +1506,13 @@ robot = RobotLab.build(name: "runner", system_prompt: "...", config: config)
|
|
|
1058
1506
|
|
|
1059
1507
|
`RobotLab::ToolLoopError < RobotLab::InferenceError`
|
|
1060
1508
|
|
|
1061
|
-
Raised when the number of tool calls in a single `run()` exceeds `max_tool_rounds`. The
|
|
1509
|
+
Raised when the number of tool calls in a single `run()` exceeds `max_tool_rounds`. The message reads:
|
|
1510
|
+
|
|
1511
|
+
```
|
|
1512
|
+
Circuit breaker triggered: <N> tool calls exceeded max_tool_rounds (<M>)
|
|
1513
|
+
```
|
|
1514
|
+
|
|
1515
|
+
where `N` is the call count that tripped the breaker and `M` is the configured limit.
|
|
1062
1516
|
|
|
1063
1517
|
### Recovery after ToolLoopError
|
|
1064
1518
|
|
|
@@ -1070,6 +1524,7 @@ After a `ToolLoopError`, the chat contains a dangling `tool_use` block with no m
|
|
|
1070
1524
|
begin
|
|
1071
1525
|
robot.run("Execute all steps.")
|
|
1072
1526
|
rescue RobotLab::ToolLoopError => e
|
|
1527
|
+
# "Circuit breaker triggered: 11 tool calls exceeded max_tool_rounds (10)"
|
|
1073
1528
|
puts "Circuit breaker fired: #{e.message}"
|
|
1074
1529
|
end
|
|
1075
1530
|
|
|
@@ -1081,9 +1536,34 @@ puts robot.config.max_tool_rounds # still set — config unchanged
|
|
|
1081
1536
|
result = robot.run("Something new.")
|
|
1082
1537
|
```
|
|
1083
1538
|
|
|
1539
|
+
## Doom Loop Detection
|
|
1540
|
+
|
|
1541
|
+
Distinct from the circuit breaker, doom-loop detection is **always on**. Every
|
|
1542
|
+
`run()` unconditionally installs a `RobotLab::DoomLoopDetector` over the chat's
|
|
1543
|
+
`execute_tool`, and removes it again when the run ends. `doom_loop_threshold:`
|
|
1544
|
+
only *tunes* it; it cannot be disabled from the constructor.
|
|
1545
|
+
|
|
1546
|
+
```ruby
|
|
1547
|
+
robot = RobotLab.build(name: "worker", system_prompt: "...", doom_loop_threshold: 5)
|
|
1548
|
+
```
|
|
1549
|
+
|
|
1550
|
+
| | |
|
|
1551
|
+
|---|---|
|
|
1552
|
+
| Default threshold | `RobotLab::DoomLoopDetector::DEFAULT_THRESHOLD` (3) |
|
|
1553
|
+
| Set via | `doom_loop_threshold:` constructor kwarg or `RunConfig#doom_loop_threshold` |
|
|
1554
|
+
|
|
1555
|
+
When a consecutive or cyclic repetition of the same tool name exceeds the
|
|
1556
|
+
threshold, the detector does **not** raise. It appends a self-correction warning
|
|
1557
|
+
to that tool's result so the model can change strategy: a `String` result gets
|
|
1558
|
+
`"\n\n⚠️ <warning>"` appended, and a `Hash` result gains a `:_doom_loop_warning`
|
|
1559
|
+
key. The detector then resets.
|
|
1560
|
+
|
|
1084
1561
|
## Learning Accumulation
|
|
1085
1562
|
|
|
1086
|
-
`robot.learn(text)` records a cross-run observation. On each subsequent `run()`,
|
|
1563
|
+
`robot.learn(text)` records a cross-run observation. On each subsequent `run()`,
|
|
1564
|
+
**all** accumulated learnings are prepended to the user message as a
|
|
1565
|
+
`LEARNINGS FROM PREVIOUS RUNS:` block. There is no active/inactive distinction —
|
|
1566
|
+
every entry in `robot.learnings` is injected.
|
|
1087
1567
|
|
|
1088
1568
|
### learn
|
|
1089
1569
|
|
|
@@ -1097,7 +1577,17 @@ Add a learning to the robot's accumulated observations. Learnings are automatica
|
|
|
1097
1577
|
- If the new text is a substring of an existing learning, it is dropped (the existing broader learning already covers it).
|
|
1098
1578
|
- If an existing learning is a substring of the new text, the narrower one is replaced.
|
|
1099
1579
|
|
|
1100
|
-
Learnings are
|
|
1580
|
+
Learnings are written to the robot's own memory under `memory[:learnings]`.
|
|
1581
|
+
|
|
1582
|
+
!!! note "Learnings do not survive process restart on their own"
|
|
1583
|
+
`initialize_memory` always constructs a fresh `Memory.new`, and there is no
|
|
1584
|
+
`memory:` constructor keyword, so a newly built robot starts with an empty
|
|
1585
|
+
`:learnings` key. `learn` reads back whatever is already in `memory[:learnings]`
|
|
1586
|
+
at construction, which means persistence requires an external store — for
|
|
1587
|
+
example the `robot_lab-durable` extension — to repopulate it.
|
|
1588
|
+
|
|
1589
|
+
`learn` runs inside the `:learn` hook family (`before_learn` / `around_learn` /
|
|
1590
|
+
`after_learn`, plus `on_learn`), so a hook handler can observe or veto the write.
|
|
1101
1591
|
|
|
1102
1592
|
**Parameters:**
|
|
1103
1593
|
|