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.
Files changed (180) hide show
  1. checksums.yaml +4 -4
  2. data/.architecture/AGENTS.md +32 -0
  3. data/.architecture/config.yml +8 -0
  4. data/.architecture/members.yml +60 -0
  5. data/.architecture/reviews/feature-free-will.md +490 -0
  6. data/.architecture/reviews/overall-codebase.md +427 -0
  7. data/.claude/settings.local.json +57 -0
  8. data/.codex/config.toml +2 -0
  9. data/.envrc +1 -0
  10. data/.irbrc +2 -2
  11. data/.loki +60 -0
  12. data/.quality/reek_baseline.txt +43 -0
  13. data/.rubocop.yml +10 -0
  14. data/CHANGELOG.md +126 -0
  15. data/CLAUDE.md +139 -0
  16. data/README.md +130 -73
  17. data/Rakefile +131 -9
  18. data/agent2agent_review.md +192 -0
  19. data/agentf_improvements.md +253 -0
  20. data/agents.md +14 -0
  21. data/docs/api/core/index.md +1 -0
  22. data/docs/api/core/network.md +31 -1
  23. data/docs/api/core/robot.md +91 -5
  24. data/docs/api/errors.md +47 -2
  25. data/docs/api/index.md +3 -0
  26. data/docs/architecture/core-concepts.md +40 -0
  27. data/docs/architecture/network-orchestration.md +8 -0
  28. data/docs/architecture/robot-execution.md +1 -1
  29. data/docs/examples/index.md +37 -2
  30. data/docs/getting-started/configuration.md +39 -7
  31. data/docs/guides/building-robots.md +39 -1
  32. data/docs/guides/creating-networks.md +6 -1
  33. data/docs/guides/hooks.md +997 -0
  34. data/docs/guides/index.md +19 -14
  35. data/docs/guides/knowledge.md +9 -3
  36. data/docs/guides/observability.md +206 -7
  37. data/docs/guides/using-tools.md +69 -0
  38. data/docs/index.md +31 -4
  39. data/docs/superpowers/plans/2026-05-06-agentskills.md +1303 -0
  40. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +247 -0
  41. data/examples/.envrc +1 -0
  42. data/examples/01_simple_robot.rb +5 -9
  43. data/examples/02_tools.rb +5 -9
  44. data/examples/03_network.rb +18 -10
  45. data/examples/04_mcp.rb +21 -29
  46. data/examples/05_streaming.rb +12 -18
  47. data/examples/06_prompt_templates.rb +11 -19
  48. data/examples/07_network_memory.rb +16 -31
  49. data/examples/08_llm_config.rb +10 -22
  50. data/examples/09_chaining.rb +16 -27
  51. data/examples/10_memory.rb +12 -28
  52. data/examples/11_network_introspection.rb +15 -29
  53. data/examples/12_message_bus.rb +5 -12
  54. data/examples/13_spawn.rb +5 -10
  55. data/examples/14_rusty_circuit/.envrc +1 -0
  56. data/examples/14_rusty_circuit/comic.rb +2 -0
  57. data/examples/14_rusty_circuit/heckler.rb +1 -1
  58. data/examples/14_rusty_circuit/open_mic.rb +1 -3
  59. data/examples/14_rusty_circuit/scout.rb +2 -0
  60. data/examples/15_memory_network_and_bus/.envrc +1 -0
  61. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +6 -3
  62. data/examples/15_memory_network_and_bus/linux_writer.rb +1 -1
  63. data/examples/15_memory_network_and_bus/output/combined_article.md +6 -6
  64. data/examples/15_memory_network_and_bus/output/final_article.md +6 -8
  65. data/examples/15_memory_network_and_bus/output/linux_draft.md +4 -2
  66. data/examples/15_memory_network_and_bus/output/mac_draft.md +3 -3
  67. data/examples/15_memory_network_and_bus/output/memory.json +6 -6
  68. data/examples/15_memory_network_and_bus/output/revision_1.md +10 -11
  69. data/examples/15_memory_network_and_bus/output/revision_2.md +6 -8
  70. data/examples/15_memory_network_and_bus/output/windows_draft.md +3 -3
  71. data/examples/16_writers_room/.envrc +1 -0
  72. data/examples/16_writers_room/writers_room.rb +2 -4
  73. data/examples/17_skills.rb +8 -17
  74. data/examples/18_rails/Gemfile +1 -0
  75. data/examples/19_token_tracking.rb +9 -15
  76. data/examples/20_circuit_breaker.rb +10 -19
  77. data/examples/21_learning_loop.rb +11 -20
  78. data/examples/22_context_compression.rb +6 -13
  79. data/examples/23_convergence.rb +6 -17
  80. data/examples/24_structured_delegation.rb +11 -15
  81. data/examples/25_history_search.rb +5 -12
  82. data/examples/26_document_store.rb +6 -13
  83. data/examples/27_incident_response/incident_response.rb +4 -5
  84. data/examples/28_mcp_discovery.rb +8 -11
  85. data/examples/29_ractor_tools.rb +4 -9
  86. data/examples/30_ractor_network.rb +10 -19
  87. data/examples/31_launch_assessment.rb +10 -23
  88. data/examples/32_newsletter_reader.rb +188 -0
  89. data/examples/33_stock_generator.rb +80 -0
  90. data/examples/33_stock_predictor.rb +306 -0
  91. data/examples/34_agentskills.rb +72 -0
  92. data/examples/35_hooks.rb +256 -0
  93. data/examples/README.md +8 -1
  94. data/examples/common.rb +80 -0
  95. data/examples/ruboruby.md +423 -0
  96. data/examples/xyzzy.rb +90 -0
  97. data/lib/robot_lab/agent_skill.rb +64 -0
  98. data/lib/robot_lab/agent_skill_catalog.rb +74 -0
  99. data/lib/robot_lab/ask_user.rb +2 -2
  100. data/lib/robot_lab/budget/ledger.rb +98 -0
  101. data/lib/robot_lab/bus_poller.rb +12 -5
  102. data/lib/robot_lab/capabilities.rb +84 -0
  103. data/lib/robot_lab/config/defaults.yml +10 -0
  104. data/lib/robot_lab/config.rb +5 -16
  105. data/lib/robot_lab/delegation_future.rb +1 -1
  106. data/lib/robot_lab/doom_loop_detector.rb +98 -0
  107. data/lib/robot_lab/error.rb +34 -4
  108. data/lib/robot_lab/errors.rb +45 -0
  109. data/lib/robot_lab/history_compressor.rb +4 -10
  110. data/lib/robot_lab/hook.rb +79 -0
  111. data/lib/robot_lab/hook_context.rb +194 -0
  112. data/lib/robot_lab/hook_registry.rb +55 -0
  113. data/lib/robot_lab/hooks.rb +87 -0
  114. data/lib/robot_lab/mcp/client.rb +1 -2
  115. data/lib/robot_lab/mcp/connection_poller.rb +5 -5
  116. data/lib/robot_lab/mcp/server.rb +1 -1
  117. data/lib/robot_lab/mcp/server_discovery.rb +0 -2
  118. data/lib/robot_lab/memory.rb +32 -27
  119. data/lib/robot_lab/memory_change.rb +2 -2
  120. data/lib/robot_lab/message.rb +4 -4
  121. data/lib/robot_lab/narrator.rb +87 -0
  122. data/lib/robot_lab/network.rb +87 -25
  123. data/lib/robot_lab/robot/agent_skill_matching.rb +99 -0
  124. data/lib/robot_lab/robot/budget.rb +89 -0
  125. data/lib/robot_lab/robot/bus_messaging.rb +79 -33
  126. data/lib/robot_lab/robot/history_search.rb +4 -1
  127. data/lib/robot_lab/robot/hooking.rb +56 -0
  128. data/lib/robot_lab/robot/mcp_management.rb +12 -12
  129. data/lib/robot_lab/robot/template_rendering.rb +62 -42
  130. data/lib/robot_lab/robot.rb +482 -242
  131. data/lib/robot_lab/robot_result.rb +6 -5
  132. data/lib/robot_lab/run_config.rb +27 -14
  133. data/lib/robot_lab/runnable.rb +51 -0
  134. data/lib/robot_lab/sandbox/null.rb +13 -0
  135. data/lib/robot_lab/sandbox/seatbelt.rb +104 -0
  136. data/lib/robot_lab/sandbox.rb +52 -0
  137. data/lib/robot_lab/script_tool.rb +134 -0
  138. data/lib/robot_lab/state_proxy.rb +7 -5
  139. data/lib/robot_lab/task.rb +26 -20
  140. data/lib/robot_lab/tool.rb +54 -13
  141. data/lib/robot_lab/tool_config.rb +1 -1
  142. data/lib/robot_lab/tool_manifest.rb +5 -7
  143. data/lib/robot_lab/user_message.rb +2 -2
  144. data/lib/robot_lab/version.rb +1 -1
  145. data/lib/robot_lab/waiter.rb +1 -1
  146. data/lib/robot_lab.rb +89 -51
  147. data/logfile +8 -0
  148. data/mkdocs.yml +3 -3
  149. data/robot_concurrency.md +38 -0
  150. data/simple_acp_review.md +298 -0
  151. data/temp.md +6 -0
  152. data/tool_manifest_plan.md +155 -0
  153. metadata +82 -83
  154. data/docs/examples/rails-application.md +0 -419
  155. data/docs/guides/ractor-parallelism.md +0 -364
  156. data/docs/guides/rails-integration.md +0 -681
  157. data/docs/superpowers/plans/2026-04-14-ractor-integration.md +0 -1538
  158. data/docs/superpowers/specs/2026-04-14-ractor-integration-design.md +0 -258
  159. data/lib/generators/robot_lab/install_generator.rb +0 -90
  160. data/lib/generators/robot_lab/job_generator.rb +0 -40
  161. data/lib/generators/robot_lab/robot_generator.rb +0 -55
  162. data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -42
  163. data/lib/generators/robot_lab/templates/job.rb.tt +0 -21
  164. data/lib/generators/robot_lab/templates/migration.rb.tt +0 -32
  165. data/lib/generators/robot_lab/templates/result_model.rb.tt +0 -52
  166. data/lib/generators/robot_lab/templates/robot.rb.tt +0 -31
  167. data/lib/generators/robot_lab/templates/robot_job.rb.tt +0 -18
  168. data/lib/generators/robot_lab/templates/robot_test.rb.tt +0 -34
  169. data/lib/generators/robot_lab/templates/routing_robot.rb.tt +0 -59
  170. data/lib/generators/robot_lab/templates/thread_model.rb.tt +0 -40
  171. data/lib/robot_lab/document_store.rb +0 -155
  172. data/lib/robot_lab/ractor_boundary.rb +0 -42
  173. data/lib/robot_lab/ractor_job.rb +0 -37
  174. data/lib/robot_lab/ractor_memory_proxy.rb +0 -85
  175. data/lib/robot_lab/ractor_network_scheduler.rb +0 -154
  176. data/lib/robot_lab/ractor_worker_pool.rb +0 -117
  177. data/lib/robot_lab/rails_integration/engine.rb +0 -29
  178. data/lib/robot_lab/rails_integration/job.rb +0 -158
  179. data/lib/robot_lab/rails_integration/railtie.rb +0 -51
  180. 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
- - [:material-cpu-64-bit: **Ractor Parallelism**](ractor-parallelism.md)
45
+ - [:octicons-zap-24: **Hook System**](hooks.md)
46
46
 
47
- True CPU parallelism for tools and robot pipelines via Ruby Ractors
47
+ Lifecycle hooks for instrumentation, response caching, extensions, and cross-cutting concerns
48
48
 
49
- </div>
50
-
51
- ## Framework Integration
52
-
53
- <div class="grid cards" markdown>
49
+ - [:octicons-search-24: **Knowledge & Retrieval**](knowledge.md)
54
50
 
55
- - [:material-language-ruby:{ .lg } **Rails Integration**](rails-integration.md)
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
- | [Rails Integration](rails-integration.md) | Rails application setup | 15 min |
72
- | [Observability & Safety](observability.md) | Token tracking, circuit breaker, learning loop | 10 min |
73
- | [Ractor Parallelism](ractor-parallelism.md) | CPU-parallel tools and robot pipelines | 15 min |
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) |
@@ -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
- `fastembed` is a core RobotLab dependency no optional gem required. The ONNX model is downloaded on first use.
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
- - [Example 25 — Chat History Search](../../examples/25_history_search.rb)
182
- - [Example 26 — Embedding-Based Document Store](../../examples/26_document_store.rb)
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--cost-tracking)
481
- - [Example 19 — Token & Cost Tracking](../../examples/19_token_tracking.rb)
482
- - [Example 20 — Tool Loop Circuit Breaker](../../examples/20_circuit_breaker.rb)
483
- - [Example 21 — Learning Accumulation Loop](../../examples/21_learning_loop.rb)
484
- - [Example 22 — Context Window Compression](../../examples/22_context_compression.rb)
485
- - [Example 23 — Convergence Detection](../../examples/23_convergence.rb)
486
- - [Example 24 — Structured Delegation](../../examples/24_structured_delegation.rb)
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)
@@ -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-train-car-container:{ .lg .middle } **Rails Integration**
119
+ - :material-shield-check:{ .lg .middle } **Observability & Safety**
120
120
 
121
121
  ---
122
122
 
123
- Generators, background jobs, and Turbo Stream token broadcasting for real-time streaming to the browser.
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: Rails Guide](guides/rails-integration.md)
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