robot_lab 0.1.0 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.architecture/AGENTS.md +32 -0
- data/.architecture/config.yml +8 -0
- data/.architecture/members.yml +60 -0
- data/.architecture/reviews/feature-free-will.md +490 -0
- data/.architecture/reviews/overall-codebase.md +427 -0
- data/.claude/settings.local.json +57 -0
- data/.codex/config.toml +2 -0
- data/.envrc +1 -0
- data/.irbrc +2 -2
- data/.loki +60 -0
- data/.quality/reek_baseline.txt +43 -0
- data/.rubocop.yml +10 -0
- data/CHANGELOG.md +126 -0
- data/CLAUDE.md +139 -0
- data/README.md +130 -73
- data/Rakefile +131 -9
- data/agent2agent_review.md +192 -0
- data/agentf_improvements.md +253 -0
- data/agents.md +14 -0
- data/docs/api/core/index.md +1 -0
- data/docs/api/core/network.md +31 -1
- data/docs/api/core/robot.md +91 -5
- data/docs/api/errors.md +47 -2
- data/docs/api/index.md +3 -0
- data/docs/architecture/core-concepts.md +40 -0
- data/docs/architecture/network-orchestration.md +8 -0
- data/docs/architecture/robot-execution.md +1 -1
- data/docs/examples/index.md +37 -2
- data/docs/getting-started/configuration.md +39 -7
- data/docs/guides/building-robots.md +39 -1
- data/docs/guides/creating-networks.md +6 -1
- data/docs/guides/hooks.md +997 -0
- data/docs/guides/index.md +19 -14
- data/docs/guides/knowledge.md +9 -3
- data/docs/guides/observability.md +206 -7
- data/docs/guides/using-tools.md +69 -0
- data/docs/index.md +31 -4
- data/docs/superpowers/plans/2026-05-06-agentskills.md +1303 -0
- data/docs/superpowers/specs/2026-05-06-agentskills-design.md +247 -0
- data/examples/.envrc +1 -0
- data/examples/01_simple_robot.rb +5 -9
- data/examples/02_tools.rb +5 -9
- data/examples/03_network.rb +18 -10
- data/examples/04_mcp.rb +21 -29
- data/examples/05_streaming.rb +12 -18
- data/examples/06_prompt_templates.rb +11 -19
- data/examples/07_network_memory.rb +16 -31
- data/examples/08_llm_config.rb +10 -22
- data/examples/09_chaining.rb +16 -27
- data/examples/10_memory.rb +12 -28
- data/examples/11_network_introspection.rb +15 -29
- data/examples/12_message_bus.rb +5 -12
- data/examples/13_spawn.rb +5 -10
- data/examples/14_rusty_circuit/.envrc +1 -0
- data/examples/14_rusty_circuit/comic.rb +2 -0
- data/examples/14_rusty_circuit/heckler.rb +1 -1
- data/examples/14_rusty_circuit/open_mic.rb +1 -3
- data/examples/14_rusty_circuit/scout.rb +2 -0
- data/examples/15_memory_network_and_bus/.envrc +1 -0
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +6 -3
- data/examples/15_memory_network_and_bus/linux_writer.rb +1 -1
- data/examples/15_memory_network_and_bus/output/combined_article.md +6 -6
- data/examples/15_memory_network_and_bus/output/final_article.md +6 -8
- data/examples/15_memory_network_and_bus/output/linux_draft.md +4 -2
- data/examples/15_memory_network_and_bus/output/mac_draft.md +3 -3
- data/examples/15_memory_network_and_bus/output/memory.json +6 -6
- data/examples/15_memory_network_and_bus/output/revision_1.md +10 -11
- data/examples/15_memory_network_and_bus/output/revision_2.md +6 -8
- data/examples/15_memory_network_and_bus/output/windows_draft.md +3 -3
- data/examples/16_writers_room/.envrc +1 -0
- data/examples/16_writers_room/writers_room.rb +2 -4
- data/examples/17_skills.rb +8 -17
- data/examples/18_rails/Gemfile +1 -0
- data/examples/19_token_tracking.rb +9 -15
- data/examples/20_circuit_breaker.rb +10 -19
- data/examples/21_learning_loop.rb +11 -20
- data/examples/22_context_compression.rb +6 -13
- data/examples/23_convergence.rb +6 -17
- data/examples/24_structured_delegation.rb +11 -15
- data/examples/25_history_search.rb +5 -12
- data/examples/26_document_store.rb +6 -13
- data/examples/27_incident_response/incident_response.rb +4 -5
- data/examples/28_mcp_discovery.rb +8 -11
- data/examples/29_ractor_tools.rb +4 -9
- data/examples/30_ractor_network.rb +10 -19
- data/examples/31_launch_assessment.rb +10 -23
- data/examples/32_newsletter_reader.rb +188 -0
- data/examples/33_stock_generator.rb +80 -0
- data/examples/33_stock_predictor.rb +306 -0
- data/examples/34_agentskills.rb +72 -0
- data/examples/35_hooks.rb +256 -0
- data/examples/README.md +8 -1
- data/examples/common.rb +80 -0
- data/examples/ruboruby.md +423 -0
- data/examples/xyzzy.rb +90 -0
- data/lib/robot_lab/agent_skill.rb +64 -0
- data/lib/robot_lab/agent_skill_catalog.rb +74 -0
- data/lib/robot_lab/ask_user.rb +2 -2
- data/lib/robot_lab/budget/ledger.rb +98 -0
- data/lib/robot_lab/bus_poller.rb +12 -5
- data/lib/robot_lab/capabilities.rb +84 -0
- data/lib/robot_lab/config/defaults.yml +10 -0
- data/lib/robot_lab/config.rb +5 -16
- data/lib/robot_lab/delegation_future.rb +1 -1
- data/lib/robot_lab/doom_loop_detector.rb +98 -0
- data/lib/robot_lab/error.rb +34 -4
- data/lib/robot_lab/errors.rb +45 -0
- data/lib/robot_lab/history_compressor.rb +4 -10
- 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/client.rb +1 -2
- data/lib/robot_lab/mcp/connection_poller.rb +5 -5
- data/lib/robot_lab/mcp/server.rb +1 -1
- data/lib/robot_lab/mcp/server_discovery.rb +0 -2
- data/lib/robot_lab/memory.rb +32 -27
- data/lib/robot_lab/memory_change.rb +2 -2
- data/lib/robot_lab/message.rb +4 -4
- data/lib/robot_lab/narrator.rb +87 -0
- data/lib/robot_lab/network.rb +87 -25
- data/lib/robot_lab/robot/agent_skill_matching.rb +99 -0
- data/lib/robot_lab/robot/budget.rb +89 -0
- data/lib/robot_lab/robot/bus_messaging.rb +79 -33
- data/lib/robot_lab/robot/history_search.rb +4 -1
- data/lib/robot_lab/robot/hooking.rb +56 -0
- data/lib/robot_lab/robot/mcp_management.rb +12 -12
- data/lib/robot_lab/robot/template_rendering.rb +62 -42
- data/lib/robot_lab/robot.rb +482 -242
- data/lib/robot_lab/robot_result.rb +6 -5
- data/lib/robot_lab/run_config.rb +27 -14
- 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 +134 -0
- data/lib/robot_lab/state_proxy.rb +7 -5
- data/lib/robot_lab/task.rb +26 -20
- data/lib/robot_lab/tool.rb +54 -13
- data/lib/robot_lab/tool_config.rb +1 -1
- data/lib/robot_lab/tool_manifest.rb +5 -7
- data/lib/robot_lab/user_message.rb +2 -2
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab/waiter.rb +1 -1
- data/lib/robot_lab.rb +89 -51
- data/logfile +8 -0
- data/mkdocs.yml +3 -3
- data/robot_concurrency.md +38 -0
- data/simple_acp_review.md +298 -0
- data/temp.md +6 -0
- data/tool_manifest_plan.md +155 -0
- metadata +82 -83
- data/docs/examples/rails-application.md +0 -419
- data/docs/guides/ractor-parallelism.md +0 -364
- data/docs/guides/rails-integration.md +0 -681
- data/docs/superpowers/plans/2026-04-14-ractor-integration.md +0 -1538
- data/docs/superpowers/specs/2026-04-14-ractor-integration-design.md +0 -258
- data/lib/generators/robot_lab/install_generator.rb +0 -90
- data/lib/generators/robot_lab/job_generator.rb +0 -40
- data/lib/generators/robot_lab/robot_generator.rb +0 -55
- data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -42
- data/lib/generators/robot_lab/templates/job.rb.tt +0 -21
- data/lib/generators/robot_lab/templates/migration.rb.tt +0 -32
- data/lib/generators/robot_lab/templates/result_model.rb.tt +0 -52
- data/lib/generators/robot_lab/templates/robot.rb.tt +0 -31
- data/lib/generators/robot_lab/templates/robot_job.rb.tt +0 -18
- data/lib/generators/robot_lab/templates/robot_test.rb.tt +0 -34
- data/lib/generators/robot_lab/templates/routing_robot.rb.tt +0 -59
- data/lib/generators/robot_lab/templates/thread_model.rb.tt +0 -40
- data/lib/robot_lab/document_store.rb +0 -155
- data/lib/robot_lab/ractor_boundary.rb +0 -42
- data/lib/robot_lab/ractor_job.rb +0 -37
- data/lib/robot_lab/ractor_memory_proxy.rb +0 -85
- data/lib/robot_lab/ractor_network_scheduler.rb +0 -154
- data/lib/robot_lab/ractor_worker_pool.rb +0 -117
- data/lib/robot_lab/rails_integration/engine.rb +0 -29
- data/lib/robot_lab/rails_integration/job.rb +0 -158
- data/lib/robot_lab/rails_integration/railtie.rb +0 -51
- data/lib/robot_lab/rails_integration/turbo_stream_callbacks.rb +0 -72
data/docs/guides/index.md
CHANGED
|
@@ -40,21 +40,15 @@ If you're new to RobotLab, start here:
|
|
|
40
40
|
|
|
41
41
|
- [:octicons-pulse-24: **Observability & Safety**](observability.md)
|
|
42
42
|
|
|
43
|
-
Token tracking, circuit breakers, and learning accumulation
|
|
43
|
+
Token tracking, circuit breakers, doom loop detection, auto compaction, and learning accumulation
|
|
44
44
|
|
|
45
|
-
- [:
|
|
45
|
+
- [:octicons-zap-24: **Hook System**](hooks.md)
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
Lifecycle hooks for instrumentation, response caching, extensions, and cross-cutting concerns
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
## Framework Integration
|
|
52
|
-
|
|
53
|
-
<div class="grid cards" markdown>
|
|
49
|
+
- [:octicons-search-24: **Knowledge & Retrieval**](knowledge.md)
|
|
54
50
|
|
|
55
|
-
-
|
|
56
|
-
|
|
57
|
-
Use RobotLab in Ruby on Rails applications
|
|
51
|
+
Chat history search and embedding-based document store for RAG workflows
|
|
58
52
|
|
|
59
53
|
</div>
|
|
60
54
|
|
|
@@ -68,6 +62,17 @@ If you're new to RobotLab, start here:
|
|
|
68
62
|
| [MCP Integration](mcp-integration.md) | External tool servers | 10 min |
|
|
69
63
|
| [Streaming](streaming.md) | Real-time responses | 5 min |
|
|
70
64
|
| [Memory](memory.md) | Shared data store | 5 min |
|
|
71
|
-
| [
|
|
72
|
-
| [
|
|
73
|
-
| [
|
|
65
|
+
| [Observability & Safety](observability.md) | Token tracking, circuit breaker, doom loop detection, auto compaction, learning loop | 10 min |
|
|
66
|
+
| [Hook System](hooks.md) | Lifecycle hooks, extensions, response caching, instrumentation | 15 min |
|
|
67
|
+
| [Knowledge & Retrieval](knowledge.md) | Chat history search and embedding-based document store (RAG) | 10 min |
|
|
68
|
+
|
|
69
|
+
## Extension Gems
|
|
70
|
+
|
|
71
|
+
Additional capabilities are available as separate gems:
|
|
72
|
+
|
|
73
|
+
| Gem | Description | Docs |
|
|
74
|
+
|-----|-------------|------|
|
|
75
|
+
| [robot_lab-rails](https://github.com/MadBomber/robot_lab-rails) | Rails generators, background jobs, Turbo Stream broadcasting | [Rails Integration guide](https://github.com/MadBomber/robot_lab-rails/blob/main/docs/guides/rails-integration.md) |
|
|
76
|
+
| [robot_lab-ractor](https://github.com/MadBomber/robot_lab-ractor) | True CPU parallelism for tools and robot pipelines via Ruby Ractors | [Ractor Parallelism guide](https://github.com/MadBomber/robot_lab-ractor/blob/main/docs/guides/ractor-parallelism.md) |
|
|
77
|
+
| [robot_lab-durable](https://github.com/MadBomber/robot_lab-durable) | Cross-session knowledge persistence with YAML-backed storage | [robot_lab-durable README](https://github.com/MadBomber/robot_lab-durable) |
|
|
78
|
+
| [robot_lab-document_store](https://github.com/MadBomber/robot_lab-document_store) | Embedding-based semantic document search via fastembed | [robot_lab-document_store README](https://github.com/MadBomber/robot_lab-document_store) |
|
data/docs/guides/knowledge.md
CHANGED
|
@@ -171,12 +171,18 @@ result = robot.run("Use the following context:\n#{context}\n\nQuestion: #{user_q
|
|
|
171
171
|
|
|
172
172
|
### Dependency
|
|
173
173
|
|
|
174
|
-
|
|
174
|
+
The embedding-based document store requires the [`robot_lab-document_store`](https://github.com/MadBomber/robot_lab-document_store) extension gem. Add it to your Gemfile:
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
gem "robot_lab-document_store"
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
This gem bundles `fastembed` for ONNX-based embeddings. The `BAAI/bge-small-en-v1.5` model (~23 MB) is downloaded on first use and cached in `~/.cache/fastembed/`. Without `robot_lab-document_store` loaded, calling `memory.store_document` or `memory.search_documents` raises `RobotLab::DependencyError`.
|
|
175
181
|
|
|
176
182
|
---
|
|
177
183
|
|
|
178
184
|
## See Also
|
|
179
185
|
|
|
180
186
|
- [Observability Guide](observability.md)
|
|
181
|
-
- [
|
|
182
|
-
- [
|
|
187
|
+
- [examples/25_history_search.rb](https://github.com/MadBomber/robot_lab/blob/main/examples/25_history_search.rb) — Chat History Search
|
|
188
|
+
- [examples/26_document_store.rb](https://github.com/MadBomber/robot_lab/blob/main/examples/26_document_store.rb) — Embedding-Based Document Store
|
|
@@ -4,10 +4,13 @@ Facilities that help you monitor, control, improve, and scale robot behaviour:
|
|
|
4
4
|
|
|
5
5
|
- **Token & Cost Tracking** — measure LLM usage per run and cumulatively
|
|
6
6
|
- **Tool Loop Circuit Breaker** — guard against runaway tool call loops
|
|
7
|
+
- **Doom Loop Detection** — catch cyclic or repetitive tool-call patterns before they spiral
|
|
8
|
+
- **Automatic Context Compaction** — prevent context overflow with configurable auto-compression
|
|
7
9
|
- **Learning Accumulation** — build up cross-run observations that guide future runs
|
|
8
10
|
- **Context Window Compression** — prune irrelevant history to stay within token budgets
|
|
9
11
|
- **Convergence Detection** — detect when independent agents reach the same conclusion
|
|
10
12
|
- **Structured Delegation** — synchronous inter-robot calls with duration and token metadata
|
|
13
|
+
- **Live Narration** — an opt-in console feed of what a robot is doing as it happens
|
|
11
14
|
|
|
12
15
|
---
|
|
13
16
|
|
|
@@ -89,6 +92,38 @@ result = fresh.run("Explain memoization.")
|
|
|
89
92
|
puts result.input_tokens # smallest possible — no prior history
|
|
90
93
|
```
|
|
91
94
|
|
|
95
|
+
### Budgets (Token & Cost)
|
|
96
|
+
|
|
97
|
+
Where token/cost tracking above is purely observational, `token_budget:` and `cost_budget:` make it enforceable — a `Robot` refuses to keep spending once a configured limit is reached:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
robot = RobotLab.build(
|
|
101
|
+
name: "capped",
|
|
102
|
+
system_prompt: "You are a concise assistant.",
|
|
103
|
+
token_budget: 10_000, # cumulative input + output tokens
|
|
104
|
+
cost_budget: 0.50 # cumulative $ across all runs
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Enforcement happens in two layers, backed by a thread-safe `RobotLab::Budget::Ledger`:
|
|
109
|
+
|
|
110
|
+
- **Before the call** — `run()` reserves whatever remains of each configured dimension. If a *prior* call already exhausted the budget, the reservation raises `RobotLab::BudgetExceeded` immediately, refusing the call outright before spending anything on it.
|
|
111
|
+
- **After the call** — actual usage (tokens from the result, cost from the response, when the provider reports pricing) replaces the reservation. If *this* call's actual usage pushes cumulative usage over budget, `RobotLab::InferenceError` is raised (unavoidable for the call that causes the overage, since totals aren't known until the response comes back) — the same error `token_budget` alone has always raised.
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
begin
|
|
115
|
+
robot.run("Another expensive task")
|
|
116
|
+
rescue RobotLab::BudgetExceeded => e
|
|
117
|
+
puts e.message # "budget exceeded for cost: 0.51 > 0.5" — refused before spending
|
|
118
|
+
rescue RobotLab::InferenceError => e
|
|
119
|
+
puts e.message # "Cost budget exceeded: $0.5231 used, budget is $0.5" — this call pushed it over
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
A dimension with no configured limit (e.g. `cost_budget` when only `token_budget` is set) is treated as unlimited and never raises. Both fields are also available on `RunConfig` and cascade through the same global → network → robot hierarchy as other infrastructure fields (see [RunConfig](../getting-started/configuration.md#runconfig-shared-operational-defaults)).
|
|
124
|
+
|
|
125
|
+
This is a native alternative to the hand-rolled `BudgetHook` pattern in the [Hooks guide](hooks.md#cost-enforcement) for the common case of a per-robot token or dollar ceiling; reach for a hook instead when you need cross-robot session totals or custom accounting.
|
|
126
|
+
|
|
92
127
|
---
|
|
93
128
|
|
|
94
129
|
## Tool Loop Circuit Breaker
|
|
@@ -162,6 +197,120 @@ puts result.reply # "The result is 42."
|
|
|
162
197
|
|
|
163
198
|
---
|
|
164
199
|
|
|
200
|
+
## Doom Loop Detection
|
|
201
|
+
|
|
202
|
+
### The Problem
|
|
203
|
+
|
|
204
|
+
`max_tool_rounds` stops a robot that loops forever, but it fires on quantity alone. A subtler failure is when a robot cycles through the same tool call sequence repeatedly — calling tool A, then B, then C, then A again — without hitting the round limit. This is a doom loop: the robot is working but not making progress.
|
|
205
|
+
|
|
206
|
+
### doom_loop_threshold
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
robot = RobotLab.build(
|
|
210
|
+
name: "runner",
|
|
211
|
+
system_prompt: "Execute all steps.",
|
|
212
|
+
local_tools: [StepTool],
|
|
213
|
+
doom_loop_threshold: 3
|
|
214
|
+
)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Set `doom_loop_threshold:` to the number of repetitions after which the detector fires. It catches two patterns:
|
|
218
|
+
|
|
219
|
+
- **Consecutive repetition** — `[A, A, A]` (same tool called N times in a row)
|
|
220
|
+
- **Cyclic repetition** — `[A, B, C, A, B, C, A, B, C]` (same sequence repeated N times)
|
|
221
|
+
|
|
222
|
+
When a doom loop is detected, a warning message is embedded directly in the tool result, prompting the LLM to try a fundamentally different approach. This avoids corrupting the Anthropic message format (no injected user messages between `tool_use`/`tool_result` pairs).
|
|
223
|
+
|
|
224
|
+
`doom_loop_threshold` can also be supplied via `RunConfig`:
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
config = RobotLab::RunConfig.new(doom_loop_threshold: 3)
|
|
228
|
+
robot = RobotLab.build(name: "runner", system_prompt: "...", config: config)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Complementary to max_tool_rounds
|
|
232
|
+
|
|
233
|
+
Use both together for comprehensive loop protection:
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
robot = RobotLab.build(
|
|
237
|
+
name: "executor",
|
|
238
|
+
system_prompt: "Execute every step.",
|
|
239
|
+
local_tools: [StepTool],
|
|
240
|
+
max_tool_rounds: 20, # hard ceiling on total tool calls
|
|
241
|
+
doom_loop_threshold: 3 # catches repetitive patterns early
|
|
242
|
+
)
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Automatic Context Compaction
|
|
248
|
+
|
|
249
|
+
### The Problem
|
|
250
|
+
|
|
251
|
+
Long-running robots accumulate conversation history. Eventually, the cumulative token count approaches the model's context window limit, causing API errors or degraded performance. Manually calling `compress_history` at the right moment requires application-level bookkeeping.
|
|
252
|
+
|
|
253
|
+
### auto_compact
|
|
254
|
+
|
|
255
|
+
Set `auto_compact:` to have the robot compress its history automatically before each `run()`:
|
|
256
|
+
|
|
257
|
+
```ruby
|
|
258
|
+
# Compact when estimated token usage exceeds 80% of the model's context window
|
|
259
|
+
robot = RobotLab.build(
|
|
260
|
+
name: "analyst",
|
|
261
|
+
system_prompt: "You are a research analyst.",
|
|
262
|
+
auto_compact: :context_window
|
|
263
|
+
)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Tuning the Threshold
|
|
267
|
+
|
|
268
|
+
`compact_threshold:` sets the fraction of the model's context window that triggers compaction. Defaults to `0.80` (80%):
|
|
269
|
+
|
|
270
|
+
```ruby
|
|
271
|
+
robot = RobotLab.build(
|
|
272
|
+
name: "analyst",
|
|
273
|
+
system_prompt: "You are a research analyst.",
|
|
274
|
+
auto_compact: :context_window,
|
|
275
|
+
compact_threshold: 0.70 # compact earlier, at 70%
|
|
276
|
+
)
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Application-Owned Compaction
|
|
280
|
+
|
|
281
|
+
Pass a `Proc` to take full control — the proc decides both when and how to compact:
|
|
282
|
+
|
|
283
|
+
```ruby
|
|
284
|
+
robot = RobotLab.build(
|
|
285
|
+
name: "analyst",
|
|
286
|
+
system_prompt: "You are a research analyst.",
|
|
287
|
+
auto_compact: ->(r) {
|
|
288
|
+
r.compress_history(recent_turns: 5) if r.chat.messages.size > 40
|
|
289
|
+
}
|
|
290
|
+
)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
The proc receives the robot instance and is called before every `run()` when messages are non-empty.
|
|
294
|
+
|
|
295
|
+
### Options
|
|
296
|
+
|
|
297
|
+
| Value | Behaviour |
|
|
298
|
+
|-------|-----------|
|
|
299
|
+
| `nil` / `:none` | No automatic compaction (default) |
|
|
300
|
+
| `:context_window` | Compact when estimated token usage exceeds `compact_threshold` fraction of model's context window |
|
|
301
|
+
| `Proc` | Called with the robot; application decides when and how to compact |
|
|
302
|
+
|
|
303
|
+
Via `RunConfig`:
|
|
304
|
+
|
|
305
|
+
```ruby
|
|
306
|
+
config = RobotLab::RunConfig.new(auto_compact: :context_window, compact_threshold: 0.75)
|
|
307
|
+
robot = RobotLab.build(name: "analyst", system_prompt: "...", config: config)
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Requires the `classifier` gem (`~> 2.3`) when using `:context_window`. Without it, a `RobotLab::DependencyError` is caught and logged rather than raised, so the robot continues running uncompressed.
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
165
314
|
## Learning Accumulation
|
|
166
315
|
|
|
167
316
|
### The Problem
|
|
@@ -238,6 +387,21 @@ end
|
|
|
238
387
|
|
|
239
388
|
After all three runs, `reviewer.learnings` contains up to three insights (fewer if any are subsets of others).
|
|
240
389
|
|
|
390
|
+
### Durable Learning (learn: Constructor Shorthand)
|
|
391
|
+
|
|
392
|
+
The `robot_lab-durable` gem adds automatic end-of-session learning promotion. Enable it with `learn: true` in the constructor:
|
|
393
|
+
|
|
394
|
+
```ruby
|
|
395
|
+
reviewer = RobotLab.build(
|
|
396
|
+
name: "reviewer",
|
|
397
|
+
system_prompt: "You are a Ruby code reviewer.",
|
|
398
|
+
learn: true,
|
|
399
|
+
learn_domain: "ruby_review" # optional namespace for the durable store
|
|
400
|
+
)
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
At the end of each session, the robot reflects on its observations and promotes durable insights to a YAML-backed store that persists across process restarts. On the next run, those stored insights are automatically reloaded as learnings.
|
|
404
|
+
|
|
241
405
|
### Memory Persistence
|
|
242
406
|
|
|
243
407
|
Learnings are stored in `memory[:learnings]`. They survive a robot rebuild when the same `Memory` object is passed to the new robot:
|
|
@@ -475,12 +639,47 @@ puts "#{analysis.robot_name} (#{analysis.duration.round(2)}s): #{analysis.reply}
|
|
|
475
639
|
|
|
476
640
|
---
|
|
477
641
|
|
|
642
|
+
## Live Narration (`RobotLab::Narrator`)
|
|
643
|
+
|
|
644
|
+
`RobotLab::Narrator` is an opt-in [Hook](hooks.md) that narrates what a robot is doing as it happens, to `$stderr` (or any `IO`), so a run is never silent between events. It complements `RobotLab::Audit` (the `robot_lab-audit` gem, which records a persistent history for post-mortem analysis) with a live, human-facing console feed — the two are independent hooks and can both be registered at once.
|
|
645
|
+
|
|
646
|
+
Enable it globally (applies to every robot run, including networks):
|
|
647
|
+
|
|
648
|
+
```ruby
|
|
649
|
+
RobotLab::Narrator.enable! # narrate to $stderr
|
|
650
|
+
RobotLab::Narrator.enable!(output: $stdout) # or any IO
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
Or register it like any hook for a narrower scope:
|
|
654
|
+
|
|
655
|
+
```ruby
|
|
656
|
+
robot.on(RobotLab::Narrator)
|
|
657
|
+
network.on(RobotLab::Narrator)
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
Once registered, a run prints a line per event:
|
|
661
|
+
|
|
662
|
+
```
|
|
663
|
+
· math_bot: thinking…
|
|
664
|
+
· → calculate operation="add"
|
|
665
|
+
· math_bot: thinking…
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
- Before each LLM call: `"<robot name>: thinking…"`
|
|
669
|
+
- Before each tool call: `"→ <tool name> <first arg>=<value>"` — only the *first* argument is shown, truncated to 80 characters
|
|
670
|
+
- After a tool call: `" ✗ <error message>"` — printed only when the tool raised; silent on success
|
|
671
|
+
|
|
672
|
+
Narrator uses `IO#puts` rather than `Kernel#warn`, since `warn` is silenced whenever Ruby warnings are disabled (`$VERBOSE` is `nil`, the common case under `bundle exec`). All three hooks rescue internally, so a narration failure never breaks the underlying run.
|
|
673
|
+
|
|
674
|
+
---
|
|
675
|
+
|
|
478
676
|
## See Also
|
|
479
677
|
|
|
480
|
-
- [Robot API](../api/core/robot.md#token
|
|
481
|
-
- [
|
|
482
|
-
- [
|
|
483
|
-
- [
|
|
484
|
-
- [
|
|
485
|
-
- [
|
|
486
|
-
- [
|
|
678
|
+
- [Robot API](../api/core/robot.md#token-cost-tracking)
|
|
679
|
+
- [examples/19_token_tracking.rb](https://github.com/MadBomber/robot_lab/blob/main/examples/19_token_tracking.rb) — Token & Cost Tracking
|
|
680
|
+
- [examples/20_circuit_breaker.rb](https://github.com/MadBomber/robot_lab/blob/main/examples/20_circuit_breaker.rb) — Tool Loop Circuit Breaker
|
|
681
|
+
- [examples/21_learning_loop.rb](https://github.com/MadBomber/robot_lab/blob/main/examples/21_learning_loop.rb) — Learning Accumulation Loop
|
|
682
|
+
- [examples/22_context_compression.rb](https://github.com/MadBomber/robot_lab/blob/main/examples/22_context_compression.rb) — Context Window Compression
|
|
683
|
+
- [examples/23_convergence.rb](https://github.com/MadBomber/robot_lab/blob/main/examples/23_convergence.rb) — Convergence Detection
|
|
684
|
+
- [examples/24_structured_delegation.rb](https://github.com/MadBomber/robot_lab/blob/main/examples/24_structured_delegation.rb) — Structured Delegation
|
|
685
|
+
- [RunConfig reference](../getting-started/configuration.md#runconfig-shared-operational-defaults)
|
data/docs/guides/using-tools.md
CHANGED
|
@@ -133,6 +133,15 @@ robot = RobotLab.build(
|
|
|
133
133
|
)
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
+
`local_tools:` and `tools:` are different mechanisms — don't confuse them. `local_tools:` attaches tool **instances or classes**; `tools:` (see [Runtime Tool Filtering](#runtime-tool-filtering) below) is a **name allowlist** that filters which of the already-attached tools are sent for a given turn. Passing an instance or class to `tools:` raises `ArgumentError` immediately, naming the offending class and pointing you at `local_tools:` instead:
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
RobotLab.build(name: "bot", tools: [GetWeather.new])
|
|
140
|
+
# => ArgumentError: `tools:` expects tool names (String/Symbol) to allow, but
|
|
141
|
+
# received GetWeather. To attach tool instances or classes, pass them as
|
|
142
|
+
# `local_tools:` (e.g. RobotLab.build(local_tools: [MyTool.new])).
|
|
143
|
+
```
|
|
144
|
+
|
|
136
145
|
### Via Template Front Matter
|
|
137
146
|
|
|
138
147
|
Declare tool class names in the template's YAML front matter. RobotLab resolves each string to a Ruby constant via `Object.const_get` and instantiates it:
|
|
@@ -163,6 +172,66 @@ robot = RobotLab.build(name: "assistant", system_prompt: "...")
|
|
|
163
172
|
robot.with_tools(GetWeather, CalculatorTool)
|
|
164
173
|
```
|
|
165
174
|
|
|
175
|
+
## Runtime Tool Filtering
|
|
176
|
+
|
|
177
|
+
`tools:` (and `mcp:`) also work as a **per-run** override, passed to `run()` itself, on top of the build-time/network/global hierarchy described in [Hierarchical MCP and Tools](../getting-started/configuration.md#hierarchical-mcp-and-tools):
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
robot.run("What's the weather?", tools: :inherit) # every attached tool
|
|
181
|
+
robot.run("Just chat, no tools needed.", tools: :none) # zero tools this turn
|
|
182
|
+
robot.run("Only use the calculator.", tools: %w[calculator]) # allowlist by name
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
| Value | Meaning |
|
|
186
|
+
|-------|---------|
|
|
187
|
+
| `:inherit` | No filter — use every tool attached at the robot/network/global level |
|
|
188
|
+
| `:none`, `[]`, `nil` | Send **zero** tools this turn |
|
|
189
|
+
| `["name", ...]` | Only these tool names, by exact match |
|
|
190
|
+
|
|
191
|
+
An explicit `:none`/`[]` is useful for a relevance filter that decided no tool is useful for the current message — it now genuinely sends zero tools for that turn (previously an empty allowlist was silently treated as "all tools," which could overflow small-context local models with the full tool set). Each turn's resolved tool set fully **replaces** the chat's tools rather than accumulating, so a later `:none` turn correctly clears whatever a prior turn attached.
|
|
192
|
+
|
|
193
|
+
> **Watch the default.** Both `Robot.new`'s and `run()`'s `tools:`/`mcp:` parameters default to `:none`, not `:inherit`. If you build a robot with `local_tools:` and then call `robot.run(message)` with no `tools:` override at all, the current runtime default takes the explicit-`:none` path above — sending no tools for that turn. Pass `tools: :inherit` explicitly (at build time, per network task, or per `run()` call — wherever fits your call site) anywhere you need the robot's attached tools available.
|
|
194
|
+
|
|
195
|
+
### Tool Capping and Per-Turn Filtering
|
|
196
|
+
|
|
197
|
+
Most LLM providers reject a tool array longer than 128 entries and fail the whole turn. RobotLab clamps the fully-resolved tool list to a ceiling right before handing it to the chat provider — the definitive choke point regardless of how the tools were configured, filtered, or MCP-connected:
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
robot = RobotLab.build(
|
|
201
|
+
name: "power_user",
|
|
202
|
+
system_prompt: "...",
|
|
203
|
+
local_tools: many_tools, # say, 150 tools
|
|
204
|
+
config: RobotLab::RunConfig.new(max_tools: 50) # override the default ceiling
|
|
205
|
+
)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
- Default ceiling: **128** tools per turn (`RobotLab::Robot::DEFAULT_MAX_TOOLS`)
|
|
209
|
+
- Override with `max_tools:` on `RunConfig` (or the `max_tools:` cascade field — see [Available Fields](../getting-started/configuration.md#available-fields)); `nil` or `<= 0` disables the cap
|
|
210
|
+
- When a turn's resolved tools exceed the cap, RobotLab logs a warning naming how many were dropped and sends the first `max_tools` entries
|
|
211
|
+
|
|
212
|
+
## Skill Scripts and Sandboxing
|
|
213
|
+
|
|
214
|
+
A skill bundle (a directory with a `SKILL.md` plus `scripts/`, discovered via `AgentSkill`) can expose its scripts as tools (`ScriptTool`). Because those scripts run as real OS processes, each `SKILL.md` can declare the capabilities its scripts need directly in front matter, alongside `name`/`description`:
|
|
215
|
+
|
|
216
|
+
```markdown title="skills/deploy-checker/SKILL.md"
|
|
217
|
+
---
|
|
218
|
+
name: deploy-checker
|
|
219
|
+
description: Verifies a deployment's health before promoting it.
|
|
220
|
+
fs_read: ["./data", "/etc/hosts"]
|
|
221
|
+
fs_write: ["./out"]
|
|
222
|
+
network: true
|
|
223
|
+
timeout: 30
|
|
224
|
+
trust: external # or "core" for trusted, always-unconfined skills
|
|
225
|
+
---
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Sandboxing itself is **opt-in and off by default** — see the [`sandbox:` config section](../getting-started/configuration.md#skill-script-sandboxing-sandbox-section). When disabled, scripts run exactly as they always have, unconfined. When enabled:
|
|
229
|
+
|
|
230
|
+
- The global `sandbox:` config is a **ceiling** (`fs_read`, `fs_write`, `network`, `timeout`); each skill's front matter is its **declared** request. The script actually runs under the **intersection** of the two — a path outside the ceiling's roots is dropped even if the skill declares it, `network` requires both sides to allow it, and `timeout` is the smaller of the two.
|
|
231
|
+
- On macOS, confinement is enforced with a generated `sandbox-exec` (Seatbelt) profile: deny-by-default, with narrow allowances for the interpreter to boot, the granted read/write paths, and (optionally) the network. Notably, `$HOME` is never implicitly readable — SSH keys and cloud credentials stay out of reach unless a path under `$HOME` is explicitly granted.
|
|
232
|
+
- Off macOS, or for any skill declaring `trust: core`, sandboxing is a passthrough — confinement is currently macOS-only and is always skipped for trusted "core" skills regardless of platform.
|
|
233
|
+
- A script that runs past its `timeout` is killed (its whole process group) and reported back to the LLM as a timed-out error rather than hanging the turn.
|
|
234
|
+
|
|
166
235
|
## Parameter Types
|
|
167
236
|
|
|
168
237
|
Define parameters on `RubyLLM::Tool` subclasses using `param`:
|
data/docs/index.md
CHANGED
|
@@ -50,7 +50,7 @@ Each robot is backed by a persistent LLM chat, configured with keyword arguments
|
|
|
50
50
|
|
|
51
51
|
Mix reusable prompt behaviors into any robot. Skills expand depth-first with automatic cycle detection and config cascading.
|
|
52
52
|
|
|
53
|
-
[:octicons-arrow-right-24: Skills Guide](guides/building-robots.md#skills)
|
|
53
|
+
[:octicons-arrow-right-24: Skills Guide](guides/building-robots.md#composable-skills)
|
|
54
54
|
|
|
55
55
|
- :material-tools:{ .lg .middle } **Extensible Tools**
|
|
56
56
|
|
|
@@ -116,16 +116,43 @@ Each robot is backed by a persistent LLM chat, configured with keyword arguments
|
|
|
116
116
|
|
|
117
117
|
[:octicons-arrow-right-24: Configuration](getting-started/configuration.md)
|
|
118
118
|
|
|
119
|
-
- :material-
|
|
119
|
+
- :material-shield-check:{ .lg .middle } **Observability & Safety**
|
|
120
120
|
|
|
121
121
|
---
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
Token tracking, `max_tool_rounds` circuit breaker, `doom_loop_threshold` pattern detection, and `auto_compact` context window management keep robots reliable in production.
|
|
124
124
|
|
|
125
|
-
[:octicons-arrow-right-24:
|
|
125
|
+
[:octicons-arrow-right-24: Observability Guide](guides/observability.md)
|
|
126
|
+
|
|
127
|
+
- :material-brain:{ .lg .middle } **Runtime Skill Matching**
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
`AgentSkillMatching` selects the most relevant `AgentSkill` files from a catalog directory via semantic similarity before each `run()`, injecting only what the robot needs.
|
|
132
|
+
|
|
133
|
+
[:octicons-arrow-right-24: Using Tools](guides/using-tools.md)
|
|
134
|
+
|
|
135
|
+
- :material-book-open-outline:{ .lg .middle } **Knowledge & Retrieval**
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
`robot.search_history` for semantic search over conversation turns, and `memory.store_document` / `memory.search_documents` for embedding-based RAG.
|
|
140
|
+
|
|
141
|
+
[:octicons-arrow-right-24: Knowledge Guide](guides/knowledge.md)
|
|
126
142
|
|
|
127
143
|
</div>
|
|
128
144
|
|
|
145
|
+
## Extension Gems
|
|
146
|
+
|
|
147
|
+
These optional gems extend RobotLab with additional capabilities:
|
|
148
|
+
|
|
149
|
+
| Gem | What it adds |
|
|
150
|
+
|-----|-------------|
|
|
151
|
+
| [robot_lab-rails](https://github.com/MadBomber/robot_lab-rails) | Rails Engine, generators, `RobotLab::Job` base class, Turbo Stream broadcasting |
|
|
152
|
+
| [robot_lab-ractor](https://github.com/MadBomber/robot_lab-ractor) | CPU parallelism for `ractor_safe` tools and robot networks via Ruby Ractors |
|
|
153
|
+
| [robot_lab-durable](https://github.com/MadBomber/robot_lab-durable) | Cross-session knowledge persistence — robots accumulate and recall learned facts |
|
|
154
|
+
| [robot_lab-document_store](https://github.com/MadBomber/robot_lab-document_store) | Embedding-based semantic document search powered by fastembed |
|
|
155
|
+
|
|
129
156
|
## Quick Example
|
|
130
157
|
|
|
131
158
|
```ruby
|