robot_lab 0.2.1 → 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/.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 +185 -64
- data/Rakefile +28 -12
- data/docs/api/core/index.md +41 -14
- data/docs/api/core/memory.md +247 -29
- data/docs/api/core/network.md +285 -33
- data/docs/api/core/result.md +120 -32
- data/docs/api/core/robot.md +636 -60
- data/docs/api/core/state.md +87 -197
- data/docs/api/core/tool.md +165 -20
- data/docs/api/errors.md +152 -14
- data/docs/api/hooks.md +469 -0
- data/docs/api/index.md +83 -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 +118 -30
- data/docs/architecture/index.md +86 -11
- data/docs/architecture/message-flow.md +66 -29
- data/docs/architecture/network-orchestration.md +153 -38
- data/docs/architecture/robot-execution.md +173 -91
- 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 +299 -93
- data/docs/getting-started/installation.md +53 -41
- data/docs/getting-started/quick-start.md +51 -6
- data/docs/guides/building-robots.md +296 -50
- data/docs/guides/creating-networks.md +220 -31
- data/docs/guides/hooks.md +1084 -0
- data/docs/guides/index.md +5 -0
- data/docs/guides/knowledge.md +37 -6
- data/docs/guides/mcp-integration.md +211 -44
- data/docs/guides/memory.md +103 -12
- data/docs/guides/observability.md +166 -51
- data/docs/guides/streaming.md +184 -125
- data/docs/guides/using-tools.md +300 -11
- data/docs/index.md +37 -5
- data/examples/01_simple_robot.rb +2 -2
- data/examples/02_tools.rb +14 -4
- data/examples/03_network.rb +23 -9
- 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 +266 -0
- data/examples/README.md +203 -42
- data/examples/common.rb +79 -7
- data/examples/xyzzy.rb +97 -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 +14 -9
- 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/names.rb +402 -0
- data/lib/robot_lab/narrator.rb +87 -0
- data/lib/robot_lab/network.rb +77 -20
- data/lib/robot_lab/robot/agent_skill_matching.rb +1 -3
- data/lib/robot_lab/robot/budget.rb +89 -0
- data/lib/robot_lab/robot/bus_messaging.rb +90 -18
- 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 +18 -5
- data/lib/robot_lab/robot.rb +233 -89
- 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 +70 -15
- data/mkdocs.yml +7 -1
- metadata +41 -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
|
@@ -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,8 +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
47
|
temperature: nil,
|
|
39
48
|
top_p: nil,
|
|
40
49
|
top_k: nil,
|
|
@@ -42,15 +51,27 @@ Robot.new(
|
|
|
42
51
|
presence_penalty: nil,
|
|
43
52
|
frequency_penalty: nil,
|
|
44
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,
|
|
45
59
|
config: nil
|
|
46
60
|
)
|
|
47
61
|
```
|
|
48
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
|
+
|
|
49
70
|
### Parameters
|
|
50
71
|
|
|
51
72
|
| Name | Type | Default | Description |
|
|
52
73
|
|------|------|---------|-------------|
|
|
53
|
-
| `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"` |
|
|
54
75
|
| `template` | `Symbol`, `nil` | `nil` | Prompt template (e.g., `:assistant` loads `prompts/assistant.md`) |
|
|
55
76
|
| `system_prompt` | `String`, `nil` | `nil` | Inline system prompt (appended after template if both given) |
|
|
56
77
|
| `context` | `Hash`, `Proc` | `{}` | Variables passed to the template |
|
|
@@ -60,7 +81,7 @@ Robot.new(
|
|
|
60
81
|
| `provider` | `String`, `Symbol`, `nil` | `nil` | LLM provider for local providers (e.g., `:ollama`, `:gpustack`). Automatically sets `assume_model_exists: true` |
|
|
61
82
|
| `mcp_servers` | `Array` | `[]` | Legacy MCP server configurations |
|
|
62
83
|
| `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) |
|
|
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) |
|
|
64
85
|
| `on_tool_call` | `Proc`, `nil` | `nil` | Callback invoked when a tool is called |
|
|
65
86
|
| `on_tool_result` | `Proc`, `nil` | `nil` | Callback invoked when a tool returns a result |
|
|
66
87
|
| `on_content` | `Proc`, `nil` | `nil` | Stored streaming callback invoked with each content chunk (see [Streaming](#streaming)) |
|
|
@@ -68,18 +89,27 @@ Robot.new(
|
|
|
68
89
|
| `bus` | `TypedBus::MessageBus`, `nil` | `nil` | Optional message bus for inter-robot communication |
|
|
69
90
|
| `skills` | `Symbol`, `Array<Symbol>`, `nil` | `nil` | Skill templates to prepend (see [Skills](#skills)) |
|
|
70
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)) |
|
|
71
|
-
| `token_budget` | `Integer`, `nil` | `nil` | Raise `InferenceError` if cumulative
|
|
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)) |
|
|
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 |
|
|
72
96
|
| `config` | `RunConfig`, `nil` | `nil` | Shared config merged with explicit kwargs (see [RunConfig](#runconfig)) |
|
|
73
|
-
| `temperature` | `Float`, `nil` | `nil` | Controls randomness
|
|
74
|
-
| `top_p` | `Float`, `nil` | `nil` | Nucleus sampling threshold |
|
|
75
|
-
| `top_k` | `Integer`, `nil` | `nil` | Top-k sampling |
|
|
76
|
-
| `max_tokens` | `Integer`, `nil` | `nil` | Maximum tokens in response |
|
|
77
|
-
| `presence_penalty` | `Float`, `nil` | `nil` | Penalize based on presence |
|
|
78
|
-
| `frequency_penalty` | `Float`, `nil` | `nil` | Penalize based on frequency |
|
|
79
|
-
| `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` |
|
|
80
104
|
|
|
81
105
|
When both `config:` and explicit kwargs (e.g., `temperature:`) are provided, explicit kwargs always win.
|
|
82
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
|
+
|
|
83
113
|
## Factory Method
|
|
84
114
|
|
|
85
115
|
```ruby
|
|
@@ -91,12 +121,15 @@ robot = RobotLab.build(
|
|
|
91
121
|
enable_cache: true,
|
|
92
122
|
bus: nil, # Optional TypedBus::MessageBus
|
|
93
123
|
skills: nil, # Optional skill templates
|
|
124
|
+
config: nil, # Optional RunConfig
|
|
94
125
|
**options # All other Robot.new parameters
|
|
95
126
|
)
|
|
96
127
|
# => RobotLab::Robot
|
|
97
128
|
```
|
|
98
129
|
|
|
99
|
-
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`.
|
|
100
133
|
|
|
101
134
|
## Attributes (Read-Only)
|
|
102
135
|
|
|
@@ -120,6 +153,8 @@ If `name` is omitted, it defaults to `"robot"`.
|
|
|
120
153
|
| `total_input_tokens` | `Integer` | Cumulative input tokens sent across all `run()` calls |
|
|
121
154
|
| `total_output_tokens` | `Integer` | Cumulative output tokens received across all `run()` calls |
|
|
122
155
|
| `learnings` | `Array<String>` | Accumulated cross-run observations (see [Learning Accumulation](#learning-accumulation)) |
|
|
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 |
|
|
123
158
|
|
|
124
159
|
## Attributes (Read-Write)
|
|
125
160
|
|
|
@@ -135,7 +170,9 @@ Used by tools like [`AskUser`](tool.md#built-in-askuser) that need terminal IO.
|
|
|
135
170
|
### run
|
|
136
171
|
|
|
137
172
|
```ruby
|
|
138
|
-
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)
|
|
139
176
|
# => RobotResult
|
|
140
177
|
```
|
|
141
178
|
|
|
@@ -145,33 +182,63 @@ Primary execution method. Sends a message to the LLM with memory/MCP/tools resol
|
|
|
145
182
|
|
|
146
183
|
| Name | Type | Default | Description |
|
|
147
184
|
|------|------|---------|-------------|
|
|
148
|
-
| `message` | `String` |
|
|
149
|
-
| `network` | `
|
|
150
|
-
| `
|
|
151
|
-
| `
|
|
152
|
-
| `
|
|
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 |
|
|
191
|
+
| `mcp` | `Symbol`, `Array` | `:none` | Runtime MCP override — `:inherit` (all attached servers), `:none`/`[]` (zero this turn), or an explicit array |
|
|
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) |
|
|
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` |
|
|
155
195
|
| `&block` | `Proc` | `nil` | Per-call streaming block, receives each content chunk |
|
|
156
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
|
+
|
|
157
209
|
When both a stored `on_content` callback and a runtime block are provided, both fire (stored first, then runtime block).
|
|
158
210
|
|
|
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).
|
|
219
|
+
|
|
159
220
|
**Returns:** `RobotResult`
|
|
160
221
|
|
|
161
222
|
**Examples:**
|
|
162
223
|
|
|
163
224
|
```ruby
|
|
164
|
-
# Simple message
|
|
225
|
+
# Simple message — sends no tools, connects no MCP servers
|
|
165
226
|
result = robot.run("What is 2+2?")
|
|
166
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
|
+
|
|
167
237
|
# With runtime memory
|
|
168
238
|
result = robot.run("Summarize the data", memory: { data: report })
|
|
169
239
|
|
|
170
240
|
# With per-call streaming block
|
|
171
241
|
result = robot.run("Tell me a story") { |chunk| print chunk.content }
|
|
172
|
-
|
|
173
|
-
# With runtime overrides
|
|
174
|
-
result = robot.run("Help me", mcp: :none, tools: :none)
|
|
175
242
|
```
|
|
176
243
|
|
|
177
244
|
### model
|
|
@@ -182,6 +249,21 @@ robot.model # => "claude-sonnet-4" or nil
|
|
|
182
249
|
|
|
183
250
|
Returns the model ID string. Resolves through the underlying chat object.
|
|
184
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
|
+
|
|
185
267
|
### update
|
|
186
268
|
|
|
187
269
|
```ruby
|
|
@@ -198,29 +280,52 @@ robot.update(
|
|
|
198
280
|
|
|
199
281
|
Reconfigure the robot after construction. Returns `self` for chaining.
|
|
200
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
|
+
|
|
201
294
|
### with_* Methods (Chaining)
|
|
202
295
|
|
|
203
|
-
|
|
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:
|
|
204
299
|
|
|
205
300
|
| Method | Description |
|
|
206
301
|
|--------|-------------|
|
|
207
302
|
| `with_model(model_id)` | Change the LLM model |
|
|
208
303
|
| `with_temperature(temp)` | Set temperature |
|
|
209
|
-
| `with_top_p(value)` | Set nucleus sampling |
|
|
210
|
-
| `with_top_k(value)` | Set top-k sampling |
|
|
211
|
-
| `with_max_tokens(value)` | Set max response tokens |
|
|
212
|
-
| `with_presence_penalty(value)` | Set presence penalty |
|
|
213
|
-
| `with_frequency_penalty(value)` | Set frequency penalty |
|
|
214
|
-
| `with_stop(sequences)` | Set stop sequences |
|
|
215
304
|
| `with_instructions(prompt)` | Set system instructions |
|
|
216
305
|
| `with_tool(tool)` | Add a single tool |
|
|
217
306
|
| `with_tools(*tools)` | Add multiple tools |
|
|
218
|
-
| `with_params(**params)` | Set
|
|
307
|
+
| `with_params(**params)` | Set arbitrary provider parameters |
|
|
219
308
|
| `with_headers(**headers)` | Set custom headers |
|
|
220
309
|
| `with_schema(schema)` | Set output schema |
|
|
221
310
|
| `with_context(**ctx)` | Set context |
|
|
222
311
|
| `with_thinking(opts)` | Enable extended thinking |
|
|
223
|
-
|
|
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
|
+
```
|
|
224
329
|
|
|
225
330
|
**Example:**
|
|
226
331
|
|
|
@@ -229,6 +334,7 @@ robot = RobotLab.build(name: "bot")
|
|
|
229
334
|
robot
|
|
230
335
|
.with_model("claude-sonnet-4")
|
|
231
336
|
.with_temperature(0.7)
|
|
337
|
+
.with_params(max_tokens: 2000)
|
|
232
338
|
.with_instructions("Be concise.")
|
|
233
339
|
.run("Hello")
|
|
234
340
|
```
|
|
@@ -271,7 +377,7 @@ message = robot.send_message(to: :bob, content: "Tell me a joke.")
|
|
|
271
377
|
# => RobotMessage
|
|
272
378
|
```
|
|
273
379
|
|
|
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.
|
|
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.
|
|
275
381
|
|
|
276
382
|
**Parameters:**
|
|
277
383
|
|
|
@@ -337,6 +443,49 @@ robot.on_message do |delivery, message|
|
|
|
337
443
|
end
|
|
338
444
|
```
|
|
339
445
|
|
|
446
|
+
### respond_to_tasks
|
|
447
|
+
|
|
448
|
+
```ruby
|
|
449
|
+
robot.respond_to_tasks(auto_reply: true) { |message| "the reply content" }
|
|
450
|
+
# => self
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
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.
|
|
454
|
+
|
|
455
|
+
**Parameters:**
|
|
456
|
+
|
|
457
|
+
| Name | Type | Default | Description |
|
|
458
|
+
|------|------|---------|-------------|
|
|
459
|
+
| `auto_reply` | `Boolean` | `true` | Send the block's result back to the sender via `send_reply` |
|
|
460
|
+
| `&responder` | `Proc` | **required** | Receives the inbound `message`; return the reply content (`nil` means no reply) |
|
|
461
|
+
|
|
462
|
+
**Returns:** `self`
|
|
463
|
+
|
|
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.
|
|
465
|
+
|
|
466
|
+
```ruby
|
|
467
|
+
bob.respond_to_tasks { |message| "handled: #{message.content}" }
|
|
468
|
+
alice.send_message(to: :bob, content: "ping")
|
|
469
|
+
# bob replies "handled: ping" back to alice automatically
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### serve
|
|
473
|
+
|
|
474
|
+
```ruby
|
|
475
|
+
robot.serve(auto_reply: true)
|
|
476
|
+
# => self
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
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.
|
|
480
|
+
|
|
481
|
+
```ruby
|
|
482
|
+
bob.serve
|
|
483
|
+
alice.send_message(to: :bob, content: "Tell me a joke.")
|
|
484
|
+
# bob runs "Tell me a joke." through its LLM and replies with the result
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
Equivalent to `respond_to_tasks(auto_reply: auto_reply) { |message| run(message.content).reply }` (with Hash-content messages flattened to `"key: value"` lines first).
|
|
488
|
+
|
|
340
489
|
### spawn
|
|
341
490
|
|
|
342
491
|
```ruby
|
|
@@ -349,6 +498,8 @@ child = robot.spawn(
|
|
|
349
498
|
|
|
350
499
|
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
500
|
|
|
501
|
+
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.
|
|
502
|
+
|
|
352
503
|
**Parameters:**
|
|
353
504
|
|
|
354
505
|
| Name | Type | Default | Description |
|
|
@@ -381,6 +532,69 @@ worker2 = bot.spawn(name: "worker", system_prompt: "Worker 2")
|
|
|
381
532
|
# Messages sent to :worker are delivered to both
|
|
382
533
|
```
|
|
383
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
|
+
|
|
384
598
|
### with_bus
|
|
385
599
|
|
|
386
600
|
```ruby
|
|
@@ -456,7 +670,18 @@ Inject pre-connected MCP clients and their tools into this robot. Used by host a
|
|
|
456
670
|
```ruby
|
|
457
671
|
# Host app manages MCP connections
|
|
458
672
|
clients = { "github" => github_client }
|
|
459
|
-
|
|
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
|
|
460
685
|
|
|
461
686
|
robot.inject_mcp!(clients: clients, tools: tools)
|
|
462
687
|
```
|
|
@@ -523,6 +748,147 @@ saved = robot.messages.dup
|
|
|
523
748
|
robot.replace_messages(saved)
|
|
524
749
|
```
|
|
525
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
|
+
|
|
526
892
|
### chat_provider
|
|
527
893
|
|
|
528
894
|
```ruby
|
|
@@ -565,7 +931,23 @@ robot.to_h
|
|
|
565
931
|
# => Hash
|
|
566
932
|
```
|
|
567
933
|
|
|
568
|
-
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`).
|
|
569
951
|
|
|
570
952
|
## Memory Behavior
|
|
571
953
|
|
|
@@ -590,20 +972,60 @@ Templates are `.md` files with optional YAML front matter, loaded via `prompt_ma
|
|
|
590
972
|
robot = RobotLab.build(name: "bot", template: :assistant, context: { tone: "friendly" })
|
|
591
973
|
```
|
|
592
974
|
|
|
593
|
-
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`.
|
|
594
980
|
|
|
595
|
-
|
|
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.
|
|
596
989
|
|
|
597
990
|
**Robot Extras:** `robot_name`, `description`, `tools`, `mcp`, `skills` — applied to the robot's identity and capabilities. Constructor-provided values always take precedence.
|
|
598
991
|
|
|
599
992
|
| Key | Type | Description |
|
|
600
993
|
|-----|------|-------------|
|
|
601
|
-
| `robot_name` | `String` | Override robot name
|
|
602
|
-
| `description` | `String` | Human-readable description |
|
|
603
|
-
| `tools` | `Array
|
|
604
|
-
| `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` |
|
|
605
998
|
| `skills` | `Array<Symbol>` | Skill templates to prepend (recursive, with cycle detection) |
|
|
606
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
|
+
|
|
607
1029
|
## Skills
|
|
608
1030
|
|
|
609
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.
|
|
@@ -654,7 +1076,45 @@ robot = RobotLab.build(
|
|
|
654
1076
|
robot.config #=> RunConfig with model: "claude-sonnet-4", temperature: 0.9, ...
|
|
655
1077
|
```
|
|
656
1078
|
|
|
657
|
-
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.
|
|
658
1118
|
|
|
659
1119
|
See [Configuration: RunConfig](../../getting-started/configuration.md#runconfig-shared-operational-defaults) for full details.
|
|
660
1120
|
|
|
@@ -728,27 +1188,55 @@ robot.run("Tell me a story") { |chunk| stream_to_client(chunk.content) }
|
|
|
728
1188
|
|
|
729
1189
|
## Configuration Hierarchy
|
|
730
1190
|
|
|
731
|
-
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**.
|
|
732
1192
|
|
|
733
1193
|
```
|
|
734
1194
|
RobotLab.config (global)
|
|
735
1195
|
|
|
|
736
|
-
+-- Network (config:)
|
|
1196
|
+
+-- Network (config:) -- propagates only mcp/tools to members
|
|
737
1197
|
| |
|
|
738
|
-
| +-- Task (config:)
|
|
1198
|
+
| +-- Task (config:) -- likewise only mcp/tools
|
|
739
1199
|
| | |
|
|
740
1200
|
| | +-- Robot (config: + build-time mcp:, tools:)
|
|
741
1201
|
| | |
|
|
742
|
-
| | +--
|
|
743
|
-
| | |
|
|
744
|
-
| | +-- run() call (runtime mcp:, tools:)
|
|
1202
|
+
| | +-- run() call (runtime mcp:, tools:) <- default :none
|
|
745
1203
|
```
|
|
746
1204
|
|
|
747
1205
|
Values at each level:
|
|
748
1206
|
|
|
749
|
-
- `:none` -- no tools/MCP at this level
|
|
750
|
-
- `:inherit` -- inherit from parent level
|
|
751
|
-
- `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.
|
|
752
1240
|
|
|
753
1241
|
## Examples
|
|
754
1242
|
|
|
@@ -798,9 +1286,14 @@ robot = RobotLab.build(
|
|
|
798
1286
|
system_prompt: "You help with math.",
|
|
799
1287
|
local_tools: [Calculator]
|
|
800
1288
|
)
|
|
801
|
-
|
|
1289
|
+
|
|
1290
|
+
# run() defaults to tools: :none — pass :inherit to actually send Calculator
|
|
1291
|
+
result = robot.run("What is 15 * 7?", tools: :inherit)
|
|
802
1292
|
```
|
|
803
1293
|
|
|
1294
|
+
Note that `param` accepts only `type:`, `desc:`/`description:`, and `required:` —
|
|
1295
|
+
there is no `enum:` option. See [Tool](tool.md#param).
|
|
1296
|
+
|
|
804
1297
|
### Robot with Local Provider
|
|
805
1298
|
|
|
806
1299
|
```ruby
|
|
@@ -813,6 +1306,10 @@ robot = RobotLab.build(
|
|
|
813
1306
|
result = robot.run("Hello!")
|
|
814
1307
|
```
|
|
815
1308
|
|
|
1309
|
+
`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.
|
|
1310
|
+
|
|
1311
|
+
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.
|
|
1312
|
+
|
|
816
1313
|
### Robot with MCP
|
|
817
1314
|
|
|
818
1315
|
```ruby
|
|
@@ -826,10 +1323,15 @@ robot = RobotLab.build(
|
|
|
826
1323
|
}
|
|
827
1324
|
]
|
|
828
1325
|
)
|
|
829
|
-
|
|
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)
|
|
830
1329
|
robot.disconnect
|
|
831
1330
|
```
|
|
832
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
|
+
|
|
833
1335
|
### Robot with Skills
|
|
834
1336
|
|
|
835
1337
|
```ruby
|
|
@@ -931,7 +1433,7 @@ Every `robot.run()` returns a `RobotResult` with token counts for that call. The
|
|
|
931
1433
|
|
|
932
1434
|
```ruby
|
|
933
1435
|
robot.reset_token_totals
|
|
934
|
-
# =>
|
|
1436
|
+
# => the robot itself (returns self, so it chains)
|
|
935
1437
|
```
|
|
936
1438
|
|
|
937
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.
|
|
@@ -958,6 +1460,26 @@ robot.reset_token_totals
|
|
|
958
1460
|
puts robot.total_input_tokens # => 0
|
|
959
1461
|
```
|
|
960
1462
|
|
|
1463
|
+
### Budgets
|
|
1464
|
+
|
|
1465
|
+
`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):
|
|
1466
|
+
|
|
1467
|
+
```ruby
|
|
1468
|
+
robot = RobotLab.build(
|
|
1469
|
+
name: "capped",
|
|
1470
|
+
system_prompt: "...",
|
|
1471
|
+
token_budget: 10_000,
|
|
1472
|
+
cost_budget: 0.50
|
|
1473
|
+
)
|
|
1474
|
+
```
|
|
1475
|
+
|
|
1476
|
+
Each `run()` reserves the remaining budget for every configured dimension before the LLM call, and reconciles the reservation with actual usage after:
|
|
1477
|
+
|
|
1478
|
+
- **`RobotLab::BudgetExceeded`** — raised up front when a *prior* call already exhausted a dimension; the new call is refused before it spends anything.
|
|
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"`).
|
|
1480
|
+
|
|
1481
|
+
See [Budgets](../../guides/observability.md#budgets-token-cost) for the full walkthrough.
|
|
1482
|
+
|
|
961
1483
|
## Tool Loop Circuit Breaker
|
|
962
1484
|
|
|
963
1485
|
Set `max_tool_rounds:` to guard against a robot looping indefinitely through tool calls. After the limit is reached, `RobotLab::ToolLoopError` is raised.
|
|
@@ -984,7 +1506,13 @@ robot = RobotLab.build(name: "runner", system_prompt: "...", config: config)
|
|
|
984
1506
|
|
|
985
1507
|
`RobotLab::ToolLoopError < RobotLab::InferenceError`
|
|
986
1508
|
|
|
987
|
-
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.
|
|
988
1516
|
|
|
989
1517
|
### Recovery after ToolLoopError
|
|
990
1518
|
|
|
@@ -996,6 +1524,7 @@ After a `ToolLoopError`, the chat contains a dangling `tool_use` block with no m
|
|
|
996
1524
|
begin
|
|
997
1525
|
robot.run("Execute all steps.")
|
|
998
1526
|
rescue RobotLab::ToolLoopError => e
|
|
1527
|
+
# "Circuit breaker triggered: 11 tool calls exceeded max_tool_rounds (10)"
|
|
999
1528
|
puts "Circuit breaker fired: #{e.message}"
|
|
1000
1529
|
end
|
|
1001
1530
|
|
|
@@ -1007,9 +1536,34 @@ puts robot.config.max_tool_rounds # still set — config unchanged
|
|
|
1007
1536
|
result = robot.run("Something new.")
|
|
1008
1537
|
```
|
|
1009
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
|
+
|
|
1010
1561
|
## Learning Accumulation
|
|
1011
1562
|
|
|
1012
|
-
`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.
|
|
1013
1567
|
|
|
1014
1568
|
### learn
|
|
1015
1569
|
|
|
@@ -1023,7 +1577,17 @@ Add a learning to the robot's accumulated observations. Learnings are automatica
|
|
|
1023
1577
|
- If the new text is a substring of an existing learning, it is dropped (the existing broader learning already covers it).
|
|
1024
1578
|
- If an existing learning is a substring of the new text, the narrower one is replaced.
|
|
1025
1579
|
|
|
1026
|
-
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.
|
|
1027
1591
|
|
|
1028
1592
|
**Parameters:**
|
|
1029
1593
|
|
|
@@ -1084,6 +1648,18 @@ robot.learn("avoid using puts and p in production code")
|
|
|
1084
1648
|
# => broader learning replaces narrower; robot.learnings.size == 1
|
|
1085
1649
|
```
|
|
1086
1650
|
|
|
1651
|
+
## Runnable Protocol
|
|
1652
|
+
|
|
1653
|
+
`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:
|
|
1654
|
+
|
|
1655
|
+
| Method | Returns |
|
|
1656
|
+
|--------|---------|
|
|
1657
|
+
| `crew` | `[self]` — a robot is a crew of one |
|
|
1658
|
+
| `chief` | `self` |
|
|
1659
|
+
| `robot_count` | `1` |
|
|
1660
|
+
| `network?` | `false` |
|
|
1661
|
+
| `single?` | `true` |
|
|
1662
|
+
|
|
1087
1663
|
## See Also
|
|
1088
1664
|
|
|
1089
1665
|
- [Building Robots Guide](../../guides/building-robots.md) (includes [Composable Skills](../../guides/building-robots.md#composable-skills))
|