robot_lab 0.2.6 → 0.2.8

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 (153) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +4 -0
  3. data/.loki +5 -0
  4. data/Archspec.rb +44 -0
  5. data/CHANGELOG.md +17 -1
  6. data/README.md +124 -64
  7. data/Rakefile +6 -111
  8. data/_typos.toml +21 -0
  9. data/docs/api/core/index.md +41 -15
  10. data/docs/api/core/memory.md +247 -29
  11. data/docs/api/core/network.md +255 -33
  12. data/docs/api/core/result.md +120 -32
  13. data/docs/api/core/robot.md +551 -61
  14. data/docs/api/core/state.md +87 -197
  15. data/docs/api/core/tool.md +165 -20
  16. data/docs/api/errors.md +110 -17
  17. data/docs/api/hooks.md +469 -0
  18. data/docs/api/index.md +80 -7
  19. data/docs/api/mcp/client.md +129 -35
  20. data/docs/api/mcp/index.md +164 -23
  21. data/docs/api/mcp/server.md +27 -3
  22. data/docs/api/mcp/transports.md +94 -22
  23. data/docs/api/messages/index.md +26 -3
  24. data/docs/api/messages/text-message.md +33 -11
  25. data/docs/api/messages/tool-call-message.md +27 -4
  26. data/docs/api/messages/tool-result-message.md +23 -4
  27. data/docs/api/messages/user-message.md +45 -8
  28. data/docs/api/skills.md +423 -0
  29. data/docs/api/streaming/context.md +28 -5
  30. data/docs/api/streaming/index.md +57 -11
  31. data/docs/api/support.md +846 -0
  32. data/docs/architecture/core-concepts.md +79 -31
  33. data/docs/architecture/index.md +87 -11
  34. data/docs/architecture/message-flow.md +66 -29
  35. data/docs/architecture/network-orchestration.md +145 -38
  36. data/docs/architecture/robot-execution.md +172 -90
  37. data/docs/architecture/state-management.md +31 -12
  38. data/docs/concepts.md +176 -21
  39. data/docs/examples/basic-chat.md +72 -19
  40. data/docs/examples/index.md +117 -31
  41. data/docs/examples/mcp-server.md +154 -45
  42. data/docs/examples/multi-robot-network.md +91 -21
  43. data/docs/examples/tool-usage.md +104 -37
  44. data/docs/getting-started/configuration.md +288 -98
  45. data/docs/getting-started/installation.md +53 -41
  46. data/docs/getting-started/quick-start.md +51 -6
  47. data/docs/guides/building-robots.md +258 -50
  48. data/docs/guides/creating-networks.md +214 -30
  49. data/docs/guides/hooks.md +263 -54
  50. data/docs/guides/knowledge.md +35 -4
  51. data/docs/guides/mcp-integration.md +211 -44
  52. data/docs/guides/memory.md +103 -12
  53. data/docs/guides/observability.md +95 -47
  54. data/docs/guides/streaming.md +184 -125
  55. data/docs/guides/using-tools.md +247 -18
  56. data/docs/index.md +36 -4
  57. data/examples/01_simple_robot.rb +2 -2
  58. data/examples/02_tools.rb +14 -4
  59. data/examples/03_network.rb +12 -7
  60. data/examples/04_mcp.rb +11 -4
  61. data/examples/05_streaming.rb +8 -5
  62. data/examples/06_prompt_templates.rb +13 -9
  63. data/examples/07_network_memory.rb +5 -5
  64. data/examples/08_llm_config.rb +20 -15
  65. data/examples/09_chaining.rb +4 -4
  66. data/examples/11_network_introspection.rb +4 -4
  67. data/examples/12_message_bus.rb +2 -2
  68. data/examples/13_spawn.rb +2 -2
  69. data/examples/14_rusty_circuit/README.md +1 -0
  70. data/examples/14_rusty_circuit/comic.rb +7 -3
  71. data/examples/14_rusty_circuit/display.rb +14 -0
  72. data/examples/14_rusty_circuit/heckler.rb +8 -6
  73. data/examples/14_rusty_circuit/open_mic.rb +17 -6
  74. data/examples/14_rusty_circuit/scout.rb +17 -10
  75. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +14 -10
  76. data/examples/15_memory_network_and_bus/linux_writer.rb +2 -2
  77. data/examples/15_memory_network_and_bus/os_editor.rb +3 -1
  78. data/examples/15_memory_network_and_bus/os_writer.rb +4 -1
  79. data/examples/16_writers_room/writer.rb +22 -22
  80. data/examples/16_writers_room/writers_room.rb +2 -0
  81. data/examples/17_skills.rb +14 -13
  82. data/examples/18_rails/README.md +20 -1
  83. data/examples/18_rails/app/controllers/chat_controller.rb +5 -1
  84. data/examples/18_rails/app/jobs/robot_run_job.rb +11 -5
  85. data/examples/18_rails/app/robots/chat_robot.rb +11 -0
  86. data/examples/18_rails/config/initializers/robot_lab.rb +8 -0
  87. data/examples/19_token_tracking.rb +25 -9
  88. data/examples/20_circuit_breaker.rb +10 -7
  89. data/examples/21_learning_loop.rb +42 -16
  90. data/examples/22_context_compression.rb +23 -23
  91. data/examples/23_convergence.rb +24 -17
  92. data/examples/24_structured_delegation.rb +13 -8
  93. data/examples/25_history_search.rb +12 -8
  94. data/examples/27_incident_response/incident_response.rb +31 -13
  95. data/examples/28_mcp_discovery.rb +17 -13
  96. data/examples/29_ractor_tools.rb +4 -2
  97. data/examples/30_ractor_network.rb +22 -17
  98. data/examples/31_launch_assessment.rb +20 -9
  99. data/examples/32_newsletter_reader.rb +7 -2
  100. data/examples/33_stock_predictor.rb +34 -13
  101. data/examples/34_agentskills.rb +7 -3
  102. data/examples/35_hooks.rb +18 -8
  103. data/examples/README.md +199 -45
  104. data/examples/common.rb +79 -11
  105. data/examples/xyzzy.rb +8 -1
  106. data/lib/robot_lab/agent_skill_catalog.rb +1 -0
  107. data/lib/robot_lab/ask_user.rb +2 -0
  108. data/lib/robot_lab/bus_poller.rb +2 -0
  109. data/lib/robot_lab/capabilities.rb +4 -0
  110. data/lib/robot_lab/config.rb +18 -5
  111. data/lib/robot_lab/doom_loop_detector.rb +6 -3
  112. data/lib/robot_lab/history_compressor.rb +5 -0
  113. data/lib/robot_lab/hook.rb +1 -0
  114. data/lib/robot_lab/hook_context.rb +4 -0
  115. data/lib/robot_lab/hook_registry.rb +1 -0
  116. data/lib/robot_lab/hooks.rb +6 -3
  117. data/lib/robot_lab/mcp/client.rb +2 -2
  118. data/lib/robot_lab/mcp/connection_poller.rb +16 -8
  119. data/lib/robot_lab/mcp/server_discovery.rb +1 -0
  120. data/lib/robot_lab/mcp/transports/sse.rb +3 -0
  121. data/lib/robot_lab/mcp/transports/stdio.rb +5 -0
  122. data/lib/robot_lab/mcp/transports/streamable_http.rb +4 -0
  123. data/lib/robot_lab/mcp/transports/websocket.rb +3 -0
  124. data/lib/robot_lab/memory.rb +23 -6
  125. data/lib/robot_lab/memory_change.rb +1 -0
  126. data/lib/robot_lab/message.rb +3 -0
  127. data/lib/robot_lab/names.rb +400 -0
  128. data/lib/robot_lab/network.rb +13 -6
  129. data/lib/robot_lab/robot/agent_skill_matching.rb +3 -3
  130. data/lib/robot_lab/robot/bus_messaging.rb +19 -8
  131. data/lib/robot_lab/robot/history_search.rb +2 -0
  132. data/lib/robot_lab/robot/hooking.rb +3 -0
  133. data/lib/robot_lab/robot/mcp_management.rb +11 -3
  134. data/lib/robot_lab/robot/template_rendering.rb +29 -7
  135. data/lib/robot_lab/robot.rb +81 -19
  136. data/lib/robot_lab/robot_result.rb +3 -0
  137. data/lib/robot_lab/run_config.rb +5 -0
  138. data/lib/robot_lab/script_tool.rb +20 -39
  139. data/lib/robot_lab/state_proxy.rb +1 -0
  140. data/lib/robot_lab/streaming/context.rb +1 -0
  141. data/lib/robot_lab/streaming/events.rb +1 -0
  142. data/lib/robot_lab/task.rb +2 -0
  143. data/lib/robot_lab/tool.rb +4 -0
  144. data/lib/robot_lab/user_message.rb +1 -0
  145. data/lib/robot_lab/utils.rb +2 -0
  146. data/lib/robot_lab/version.rb +1 -1
  147. data/lib/robot_lab/waiter.rb +3 -0
  148. data/lib/robot_lab.rb +27 -18
  149. data/mkdocs.yml +6 -1
  150. metadata +12 -7
  151. data/lib/robot_lab/sandbox/null.rb +0 -13
  152. data/lib/robot_lab/sandbox/seatbelt.rb +0 -104
  153. data/lib/robot_lab/sandbox.rb +0 -52
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d401853bc5fff2646793cfff4fbda7082b18b0e07b833b3f4de6545fca574125
4
- data.tar.gz: f9964095406885bed329d8f2e7189cbb582e1c4665e3d53781b83371ee9016c7
3
+ metadata.gz: de87b78206bc424703b465d3a1f236db723ea38fe4235c07413466ab5b469ead
4
+ data.tar.gz: 6a7db14e4530e615e258415491422f297567a2f640983035f1542d88e7d200e5
5
5
  SHA512:
6
- metadata.gz: dd29d885c9ea4e0b91bffe49b757808213e8011302fac48b4632c37d0cc95315a559f80240fabca92c0a309fead3f1f72b1da3ac7eccdb440059451fc53c5a0e
7
- data.tar.gz: 518c45909e25437da64e16ad69a5bfe5f361ddd602dc64c0ad475acde1c0b7946a0773fa36960a0baea3294ae7a7cef1835b6d08be2034e3a4a905fa74715eb2
6
+ metadata.gz: f6814c9a8b86b8e6d1df2adcc2d9b4ab84c176c67211424e8404e9d83c1e08ffd353587523e4c45efec295d0e47bbd731b0cdc6f3e62754cfd4f1ce2a33d7655
7
+ data.tar.gz: 37682af098203ae333cc83fa6e66a95f1883aae0bed8411524a05d6795bb47b4a99f231353fe3546b2fdae8a5c098d0f45b863e531edaf237aab33e4e1bc3859
data/.envrc CHANGED
@@ -1,2 +1,6 @@
1
+ # robot_lab_project/robot_lab/.envrc
2
+
3
+ source_up
4
+
1
5
  export RR=`pwd`
2
6
  export BUNDLE_GEMFILE=Gemfile
data/.loki CHANGED
@@ -18,6 +18,11 @@ class Tasks
18
18
  sh "ruby -Ilib:test \"#{path}\""
19
19
  end
20
20
 
21
+ desc "Check architecture boundaries with ArchSpec"
22
+ def archspec
23
+ sh "bundle exec archspec check"
24
+ end
25
+
21
26
  class DocsCommands < Tasks
22
27
  desc "Build YARD + MkDocs documentation"
23
28
  def build
data/Archspec.rb ADDED
@@ -0,0 +1,44 @@
1
+ component :robot, in: "lib/robot_lab/robot.rb"
2
+ component :robot_support, in: "lib/robot_lab/robot/**/*.rb"
3
+ component :network, in: ["lib/robot_lab/network.rb", "lib/robot_lab/bus_poller.rb"]
4
+ component :memory, in: ["lib/robot_lab/memory.rb", "lib/robot_lab/memory_change.rb"]
5
+ component :mcp, in: "lib/robot_lab/mcp/**/*.rb"
6
+ component :sandbox, in: ["lib/robot_lab/sandbox.rb", "lib/robot_lab/sandbox/**/*.rb"]
7
+ component :streaming, in: "lib/robot_lab/streaming/**/*.rb"
8
+ component :budget, in: "lib/robot_lab/budget/**/*.rb"
9
+ component :hooks, in: %w[
10
+ lib/robot_lab/hook.rb
11
+ lib/robot_lab/hook_context.rb
12
+ lib/robot_lab/hook_registry.rb
13
+ lib/robot_lab/hooks.rb
14
+ ]
15
+ component :tools, in: %w[
16
+ lib/robot_lab/tool.rb
17
+ lib/robot_lab/tool_config.rb
18
+ lib/robot_lab/tool_manifest.rb
19
+ lib/robot_lab/script_tool.rb
20
+ ]
21
+ component :config, in: ["lib/robot_lab/config.rb", "lib/robot_lab/run_config.rb"]
22
+
23
+ # NOTE: every file here reopens `module RobotLab`, so a `dependencies.forbid`
24
+ # rule keyed on components (e.g. `memory.cannot_use :robot`) treats a plain
25
+ # `RobotLab.config` call as "depends on every component" — every file
26
+ # "defines" the bare RobotLab constant. Naming the actual target constants
27
+ # instead of components sidesteps that ambiguity.
28
+
29
+ # Memory is a low-level primitive shared by Robot and Network; it must not
30
+ # depend back on the things that depend on it.
31
+ memory.cannot_reference_constants "RobotLab::Robot", "RobotLab::Network"
32
+
33
+ # MCP is an external-tool integration layer that Robot/Network consume; it
34
+ # must not reach back into the objects that use it.
35
+ mcp.cannot_reference_constants "RobotLab::Robot", "RobotLab::Network"
36
+
37
+ # Sandbox runs untrusted/generated code paths and should have the smallest
38
+ # possible surface area — no reaching into orchestration or memory internals.
39
+ sandbox.cannot_reference_constants "RobotLab::Robot", "RobotLab::Network", "RobotLab::Memory"
40
+
41
+ # Config/RunConfig sits at the bottom of the configuration cascade
42
+ # (RobotLab.config -> Network -> Robot -> template -> task -> runtime) and
43
+ # must not depend upward on the objects it configures.
44
+ config.cannot_reference_constants "RobotLab::Robot", "RobotLab::Network"
data/CHANGELOG.md CHANGED
@@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
 
9
- ## [Unreleased]
9
+ ## [0.2.8] - 2026-09-09
10
10
 
11
11
  ### Added
12
12
  - `BusMessaging#respond_to_tasks` and `#serve` — a member auto-answers inbound
@@ -16,6 +16,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
16
16
  without hand-wiring `on_message`.
17
17
  - `.loki` Asgard task file: `test`, `rubocop`, `rubocop_fix`, `flog`, `flay`, `quality`, `build`, `install`, `release`, `integration`, `docs`, and `examples` tasks via the Asgard task runner
18
18
 
19
+ ### Changed (BREAKING)
20
+ - **Sandboxing extracted to `robot_lab-sandbox`.** `RobotLab::Sandbox`,
21
+ `RobotLab::Sandbox::Seatbelt`, and `RobotLab::Sandbox::Null` no longer live
22
+ in core; they moved verbatim to the new `robot_lab-sandbox` gem. Core has no
23
+ execution limitations of its own: `ScriptTool.execute` now delegates to an
24
+ optional `ScriptTool.executor` (`nil` by default, in which case it runs the
25
+ unconfined `Open3.capture2e` path with no timeout exactly as before).
26
+ Applications that want confined skill-script execution must add
27
+ `gem "robot_lab-sandbox"` and `require "robot_lab/sandbox"`, which installs
28
+ itself as the executor; `config.sandbox.*` behavior is unchanged from there.
29
+ `RobotLab::Capabilities` (the declarative fs_read/fs_write/network/timeout/
30
+ trust manifest parsed from `SKILL.md`) stays in core since `AgentSkill` needs
31
+ it regardless of whether sandboxing is enforced.
32
+ - `ScriptTool.run_with_timeout` and `ScriptTool.terminate` moved to
33
+ `RobotLab::Sandbox::Executor` in `robot_lab-sandbox`.
34
+
19
35
  ### Fixed
20
36
  - `BusMessaging` now synchronizes its message counter and outbox with a mutex, so
21
37
  concurrent sends and poller-thread reply correlation can't clobber each other.
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # RobotLab
2
2
 
3
- > [!INFO]
4
- > See the [CHANGELOG](CHANGELOG.md) for the latest changes. The [examples directory has a good cross section of demo apps](examples/README.md) that show-off the various capabilities of the RobotLab library.
3
+ > [!NOTE]
4
+ > See the [CHANGELOG](CHANGELOG.md) for the latest changes. The [examples directory has a good cross section of demo apps](examples/README.md) that show-off the various capabilities of the RobotLab library.
5
5
 
6
6
  <br>
7
7
  <table>
@@ -21,7 +21,7 @@
21
21
  - <strong>Human-in-the-Loop</strong> - AskUser tool for interactive prompting<br>
22
22
  - <strong>Content Streaming</strong> - Stored callbacks, per-call blocks, or both<br>
23
23
  - <strong>MCP Integration</strong> - Connect to external tool servers with timeouts and retry<br>
24
- - <strong>Local LLM Providers</strong> - Ollama, GPUStack, LM Studio via provider passthrough<br>
24
+ - <strong>Local LLM Providers</strong> - Ollama and GPUStack via provider passthrough<br>
25
25
  - <strong>Shared Memory</strong> - Reactive key-value store with subscriptions<br>
26
26
  - <strong>Message Bus</strong> - Bidirectional robot communication via TypedBus<br>
27
27
  - <strong>Dynamic Spawning</strong> - Robots create new robots at runtime<br>
@@ -30,6 +30,7 @@
30
30
  - <strong>Rails Integration</strong> - Generators, background jobs, Turbo Stream broadcasting (via <a href="https://github.com/MadBomber/robot_lab-rails">robot_lab-rails</a>)<br>
31
31
  - <strong>Token &amp; Cost Tracking</strong> - Per-run and cumulative token counts on every robot<br>
32
32
  - <strong>Tool Loop Circuit Breaker</strong> - <code>max_tool_rounds:</code> guards against runaway tool call loops<br>
33
+ - <strong>Doom Loop Detection</strong> - always-on detection of repeated or cyclic tool call patterns, tunable via <code>doom_loop_threshold:</code><br>
33
34
  - <strong>Learning Accumulation</strong> - <code>robot.learn()</code> builds up cross-run observations with deduplication<br>
34
35
  - <strong>Context Window Compression</strong> - <code>robot.compress_history()</code> prunes irrelevant old turns via TF cosine scoring<br>
35
36
  - <strong>Convergence Detection</strong> - <code>RobotLab::Convergence</code> detects when independent agents agree, enabling reconciler fast-path<br>
@@ -38,7 +39,7 @@
38
39
  </tr>
39
40
  </table>
40
41
 
41
- <p>RobotLab enables sophisticated AI applications using multiple specialized robots (LLM agents) that work together to accomplish complex tasks. Each robot has its own instructions, skills, tools, and capabilities. Review the [full documentation website](https://madbomber.github.io/robot_lab) snd explore the [many examples](examples/README.md) available as working demo applications.</p>
42
+ <p>RobotLab enables sophisticated AI applications using multiple specialized robots (LLM agents) that work together to accomplish complex tasks. Each robot has its own instructions, skills, tools, and capabilities. Review the [full documentation website](https://madbomber.github.io/robot_lab) and explore the [many examples](examples/README.md) available as working demo applications.</p>
42
43
 
43
44
  ## Installation
44
45
 
@@ -81,7 +82,11 @@ puts result.last_text_content
81
82
 
82
83
  ### Local LLM Providers
83
84
 
84
- For local LLM providers (Ollama, GPUStack, LM Studio, etc.), use the `provider:` parameter:
85
+ For local LLM providers, use the `provider:` parameter. RubyLLM's provider list is the authority on valid values — `:ollama` and `:gpustack` are the local ones (alongside `:anthropic`, `:azure`, `:bedrock`, `:deepseek`, `:gemini`, `:mistral`, `:openai`, `:openrouter`, `:perplexity`, `:vertexai`, and `:xai`). The provider's API base must be configured first, or building the robot raises `RubyLLM::ConfigurationError`:
86
+
87
+ ```bash
88
+ export ROBOT_LAB_RUBY_LLM__OLLAMA_API_BASE=http://localhost:11434/v1
89
+ ```
85
90
 
86
91
  ```ruby
87
92
  robot = RobotLab.build(
@@ -92,13 +97,15 @@ robot = RobotLab.build(
92
97
  )
93
98
  ```
94
99
 
100
+ Specifying `provider:` also sets `assume_model_exists`, so the model name is not validated against a known-model list.
101
+
95
102
  ### Configuration
96
103
 
97
104
  RobotLab uses [MywayConfig](https://github.com/MadBomber/myway_config) for layered configuration. Configuration is loaded automatically from multiple sources in priority order:
98
105
 
99
106
  1. Bundled defaults (`lib/robot_lab/config/defaults.yml`)
100
107
  2. Environment-specific overrides (development, test, production)
101
- 3. XDG user config (`~/.config/robot_lab/config.yml`)
108
+ 3. XDG user config (`~/.config/robot_lab/robot_lab.yml` — note the filename repeats the app name)
102
109
  4. Project config (`./config/robot_lab.yml`)
103
110
  5. Environment variables (`ROBOT_LAB_*` prefix)
104
111
 
@@ -113,7 +120,6 @@ export ROBOT_LAB_RUBY_LLM__MODEL=claude-sonnet-4
113
120
  # Access configuration values
114
121
  RobotLab.config.ruby_llm.model #=> "claude-sonnet-4"
115
122
  RobotLab.config.ruby_llm.request_timeout #=> 120
116
- RobotLab.config.streaming_enabled #=> true
117
123
  ```
118
124
 
119
125
  Or create a project config file at `./config/robot_lab.yml`:
@@ -192,9 +198,10 @@ tools:
192
198
  - CodeSearchTool
193
199
  mcp:
194
200
  - name: github
195
- transport: stdio
196
- command: npx
197
- args: ["-y", "@modelcontextprotocol/server-github"]
201
+ transport:
202
+ type: stdio
203
+ command: npx
204
+ args: ["-y", "@modelcontextprotocol/server-github"]
198
205
  model: claude-sonnet-4
199
206
  ---
200
207
  You are a GitHub assistant. Use available tools to help with repository tasks.
@@ -205,7 +212,10 @@ You are a GitHub assistant. Use available tools to help with repository tasks.
205
212
  robot = RobotLab.build(template: :github_assistant)
206
213
  ```
207
214
 
208
- Front matter supports: `description`, `robot_name`, `tools`, `mcp`, `skills`, `parameters`, and LLM config keys (`model`, `temperature`, `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop`). Constructor-provided values always take precedence over front matter.
215
+ Front matter supports: `description`, `robot_name`, `tools`, `mcp`, `skills`, `parameters`, and LLM config keys. Constructor-provided values always take precedence over front matter.
216
+
217
+ > [!IMPORTANT]
218
+ > Of the LLM config keys, only **`model`** and **`temperature`** currently take effect from front matter. `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, and `stop` are parsed but not applied — set those as constructor kwargs (or via `config:`), where they do work.
209
219
 
210
220
  ### Composable Skills
211
221
 
@@ -237,7 +247,7 @@ Skills are expanded depth-first and can reference other skills (with automatic c
237
247
 
238
248
  ### Combining Templates with System Prompts
239
249
 
240
- The `system_prompt` parameter can also be used alongside a template. When both are provided, the template renders first and the `system_prompt` is appended. This is particularly useful during development and testing when you want to add temporary instructions or context to an existing template:
250
+ The `system_prompt` parameter can also be used alongside a template. When both are provided, the template renders first and the `system_prompt` is appended, producing a single combined system message. The appended text also survives template re-rendering, so it persists across `run()` calls that supply runtime context. This is particularly useful during development and testing when you want to add temporary instructions or context to an existing template:
241
251
 
242
252
  ```ruby
243
253
  robot = RobotLab.build(
@@ -250,7 +260,7 @@ robot = RobotLab.build(
250
260
 
251
261
  ### Shared Configuration with RunConfig
252
262
 
253
- `RunConfig` lets you define operational defaults that flow through the hierarchy: Network -> Robot -> Template -> Task -> Runtime. Use it to share LLM settings across multiple robots or an entire network.
263
+ `RunConfig` lets you define operational defaults. For a single robot they cascade from least- to most-specific: template front matter -> `config:` -> constructor kwargs. Front matter is the *base*, not an override constructor kwargs always win.
254
264
 
255
265
  ```ruby
256
266
  # Create a shared config
@@ -267,7 +277,7 @@ robot = RobotLab.build(
267
277
  config: shared
268
278
  )
269
279
 
270
- # Apply to an entire network (all robots inherit these defaults)
280
+ # Apply at the network level (see the note below on what actually propagates)
271
281
  network = RobotLab.create_network(name: "pipeline", config: shared) do
272
282
  task :analyzer, analyzer_robot, depends_on: :none
273
283
  task :writer, writer_robot, depends_on: [:analyzer]
@@ -282,6 +292,11 @@ robot = RobotLab.build(
282
292
  )
283
293
  ```
284
294
 
295
+ > [!IMPORTANT]
296
+ > A network-level `config:` propagates only `mcp` and `tools` down to its robots, and only when the robot opts in with `mcp: :inherit` / `tools: :inherit` — `:none` at any level resolves to an empty list. LLM fields (`model`, `temperature`, `max_tokens`, …) and callbacks (`on_content`) are read from each robot's own config at construction time, so they are **not** inherited from the network. To share LLM settings, pass the same `RunConfig` to each robot via `config:`.
297
+ >
298
+ > `max_concurrent_robots` is the one field the network itself consumes.
299
+
285
300
  RunConfig supports keyword construction, block DSL, and merge semantics:
286
301
 
287
302
  ```ruby
@@ -301,7 +316,7 @@ effective.model #=> "claude-sonnet-4"
301
316
 
302
317
  ### Chaining Configuration
303
318
 
304
- Robots support method chaining to adjust configuration after creation:
319
+ Robots support method chaining to adjust configuration after creation. The `with_*` methods are delegated dynamically from the underlying `RubyLLM::Chat`, so the available set is whatever that class defines — currently `with_context`, `with_headers`, `with_instructions`, `with_model`, `with_params`, `with_schema`, `with_temperature`, `with_thinking`, `with_tool`, and `with_tools`. Each returns the robot, so calls chain.
305
320
 
306
321
  ```ruby
307
322
  robot = RobotLab.build(name: "writer", system_prompt: "You are a creative writer.")
@@ -309,10 +324,18 @@ robot = RobotLab.build(name: "writer", system_prompt: "You are a creative writer
309
324
  result = robot
310
325
  .with_temperature(0.9)
311
326
  .with_model("claude-sonnet-4")
312
- .with_max_tokens(2000)
313
327
  .run("Write a haiku about Ruby programming")
314
328
  ```
315
329
 
330
+ There is no `with_max_tokens`. Set token limits in the constructor, or pass them through `with_params`:
331
+
332
+ ```ruby
333
+ robot = RobotLab.build(name: "writer", system_prompt: "...", max_tokens: 2000)
334
+
335
+ # or, after creation
336
+ robot.with_params(max_tokens: 2000)
337
+ ```
338
+
316
339
  ## Graceful Tool Error Handling
317
340
 
318
341
  `RobotLab::Tool` automatically catches exceptions in `execute` and returns a plain-text error to the LLM instead of crashing the run. The LLM can then reason about the error and try an alternative approach.
@@ -368,9 +391,22 @@ robot = RobotLab.build(
368
391
  local_tools: [Magic8Ball]
369
392
  )
370
393
 
371
- result = robot.run("Should I start learning Rust?")
394
+ # NOTE the `tools: :inherit` see the warning below
395
+ result = robot.run("Should I start learning Rust?", tools: :inherit)
372
396
  ```
373
397
 
398
+ > [!WARNING]
399
+ > `run()` defaults to `tools: :none` and `mcp: :none`, and `:none` means **send zero tools this turn**. Attaching tools with `local_tools:` (or servers with `mcp:`) is therefore not enough on its own — a plain `robot.run("...")` sends the LLM no tools at all and connects no MCP servers.
400
+ >
401
+ > Pass `tools: :inherit` / `mcp: :inherit` on each run to use what the robot has attached:
402
+ >
403
+ > ```ruby
404
+ > robot.run("...", tools: :inherit) # send attached tools
405
+ > robot.run("...", mcp: :inherit, tools: :inherit) # connect MCP + send its tools
406
+ > ```
407
+ >
408
+ > `:inherit` means "no filter, use everything attached"; an explicit array acts as a name allowlist. Note that `tools: :inherit` at *build* time behaves differently — it resolves against the parent level, so keep `:inherit` on the `run()` call.
409
+
374
410
  ## Orchestrating Multiple Robots
375
411
 
376
412
  Networks use [SimpleFlow](https://github.com/MadBomber/simple_flow) pipelines with optional task activation for intelligent routing:
@@ -423,24 +459,29 @@ puts result.value.last_text_content
423
459
 
424
460
  ## Memory
425
461
 
426
- Both robots and networks have inherent memory that persists across runs:
462
+ Two independent mechanisms carry state across runs, and it is worth keeping them straight:
463
+
464
+ - **Chat history** — each robot owns a persistent `RubyLLM::Chat`, so conversational context carries forward automatically with no work on your part.
465
+ - **`RobotLab::Memory`** — an explicit key-value store you read and write yourself, shared across robots when they run inside a network.
427
466
 
428
467
  ```ruby
429
- # Standalone robot with inherent memory
430
468
  robot = RobotLab.build(name: "assistant", system_prompt: "You are helpful.")
431
469
 
432
470
  robot.run("My name is Alice")
433
- robot.run("What's my name?") # Memory persists automatically
471
+ robot.run("What's my name?") # answered from the persistent chat history
434
472
 
435
- # Access robot's memory
473
+ # Access robot's memory (the explicit key-value store)
436
474
  robot.memory[:user_id] = 123
437
475
  robot.memory.data[:category] = "billing"
438
476
 
439
477
  # Runtime memory injection
440
478
  robot.run("Help me", memory: { session_id: "abc123", tier: "premium" })
441
479
 
442
- # Reset memory when needed
480
+ # Reset the key-value store (does NOT clear chat history)
443
481
  robot.reset_memory
482
+
483
+ # Clear chat history instead, keeping the system prompt
484
+ robot.clear_messages
444
485
  ```
445
486
 
446
487
  Networks pass context through SimpleFlow::Result:
@@ -467,6 +508,8 @@ billing_result = result.context[:billing]
467
508
  puts result.value.last_text_content
468
509
  ```
469
510
 
511
+ Results are keyed in `result.context` by the **robot's** `name`, not by the task name given to `task`. Keep the two identical unless you have a reason not to.
512
+
470
513
  ## MCP Integration
471
514
 
472
515
  Connect to external tool servers via Model Context Protocol:
@@ -490,17 +533,18 @@ robot = RobotLab.build(
490
533
  mcp: [filesystem_server]
491
534
  )
492
535
 
493
- # Optionally connect eagerly (default is lazy on first run)
536
+ # Connect. Do this explicitly — run() defaults to mcp: :none, so a plain
537
+ # run() never connects the servers configured above.
494
538
  robot.connect_mcp!
495
539
 
496
540
  # Check connection status
497
541
  puts "Failed: #{robot.failed_mcp_server_names}" if robot.failed_mcp_server_names.any?
498
542
 
499
- # Robot can now use filesystem tools
500
- result = robot.run("List the files in the current directory")
543
+ # Robot can now use filesystem tools — :inherit is required on the run
544
+ result = robot.run("List the files in the current directory", mcp: :inherit, tools: :inherit)
501
545
  ```
502
546
 
503
- MCP connections are resilient: failed servers are automatically retried on subsequent `run()` calls, and one failing server does not prevent others from connecting.
547
+ MCP connections are resilient: failed servers are automatically retried on subsequent `run(mcp: :inherit)` calls, and one failing server does not prevent others from connecting. Connection failures are logged and recorded in `failed_mcp_server_names` rather than raised, so a robot whose servers all failed still builds and runs — just without those tools.
504
548
 
505
549
  ## Message Bus
506
550
 
@@ -634,7 +678,7 @@ robot = RobotLab.build(
634
678
  robot.run("Tell me a story") { |chunk| stream_to_client(chunk.content) }
635
679
  ```
636
680
 
637
- The `on_content:` callback participates in the RunConfig cascade, so it can be set at the network or config level and inherited by robots.
681
+ The `on_content:` callback is a `RunConfig` field, so it can also be supplied through a robot's `config:` instead of as a constructor kwarg. It is read from the robot's own config at construction time — a network-level `config:` does not supply it.
638
682
 
639
683
  ## Token & Cost Tracking
640
684
 
@@ -675,7 +719,8 @@ robot = RobotLab.build(
675
719
  begin
676
720
  robot.run("Run all steps.")
677
721
  rescue RobotLab::ToolLoopError => e
678
- puts e.message # "Tool call limit of 10 exceeded"
722
+ puts e.message
723
+ # "Circuit breaker triggered: 11 tool calls exceeded max_tool_rounds (10)"
679
724
  end
680
725
  ```
681
726
 
@@ -688,14 +733,16 @@ result = robot.run("Something new.") # robot is healthy again
688
733
 
689
734
  ## Doom Loop Detection
690
735
 
691
- Doom loop detection catches the subtler failure mode where a robot repeats the same tool call pattern indefinitely — not hitting `max_tool_rounds`, but cycling through the same sequence over and over. Set `doom_loop_threshold:` to enable it:
736
+ Doom loop detection catches the subtler failure mode where a robot repeats the same tool call pattern indefinitely — not hitting `max_tool_rounds`, but cycling through the same sequence over and over.
737
+
738
+ Unlike the circuit breaker, doom loop detection is **always active** — it is installed on every run. `doom_loop_threshold:` tunes the sensitivity; it does not switch the feature on. The default threshold is `3`.
692
739
 
693
740
  ```ruby
694
741
  robot = RobotLab.build(
695
742
  name: "runner",
696
743
  system_prompt: "Execute steps.",
697
744
  local_tools: [StepTool],
698
- doom_loop_threshold: 3 # alert after 3 identical consecutive or cyclic sequences
745
+ doom_loop_threshold: 5 # tolerate more repetition before warning (default: 3)
699
746
  )
700
747
  ```
701
748
 
@@ -710,27 +757,33 @@ robot = RobotLab.build(name: "runner", system_prompt: "...", config: config)
710
757
 
711
758
  `auto_compact` triggers context window compression automatically before each `run()`, preventing context overflow without manual intervention.
712
759
 
760
+ `auto_compact` and `compact_threshold` are `RunConfig` fields, not constructor keywords — pass them via `config:`:
761
+
713
762
  ```ruby
714
763
  # Built-in trigger: compact when estimated token usage exceeds 80% of context window
715
764
  robot = RobotLab.build(
716
765
  name: "analyst",
717
766
  system_prompt: "You are a research analyst.",
718
- auto_compact: :context_window
767
+ config: RobotLab::RunConfig.new(auto_compact: :context_window)
719
768
  )
720
769
 
721
770
  # Tune the threshold (here: compact at 70%)
722
771
  robot = RobotLab.build(
723
772
  name: "analyst",
724
773
  system_prompt: "You are a research analyst.",
725
- auto_compact: :context_window,
726
- compact_threshold: 0.70
774
+ config: RobotLab::RunConfig.new(
775
+ auto_compact: :context_window,
776
+ compact_threshold: 0.70
777
+ )
727
778
  )
728
779
 
729
780
  # Application-owned compaction: full control over when and how
730
781
  robot = RobotLab.build(
731
782
  name: "analyst",
732
783
  system_prompt: "You are a research analyst.",
733
- auto_compact: ->(r) { r.compress_history(recent_turns: 5) if r.chat.messages.size > 40 }
784
+ config: RobotLab::RunConfig.new(
785
+ auto_compact: ->(r) { r.compress_history(recent_turns: 5) if r.chat.messages.size > 40 }
786
+ )
734
787
  )
735
788
  ```
736
789
 
@@ -740,12 +793,7 @@ robot = RobotLab.build(
740
793
  | `:context_window` | Compact when estimated token usage exceeds `compact_threshold` fraction of model's context window |
741
794
  | `Proc` | Called with the robot before each `run()`; application decides when and how to compact |
742
795
 
743
- `compact_threshold` defaults to `0.80` (80%). Requires the `classifier` gem when using the built-in `:context_window` strategy. Via `RunConfig`:
744
-
745
- ```ruby
746
- config = RobotLab::RunConfig.new(auto_compact: :context_window, compact_threshold: 0.75)
747
- robot = RobotLab.build(name: "analyst", system_prompt: "...", config: config)
748
- ```
796
+ `compact_threshold` defaults to `0.80` (80%). The built-in `:context_window` strategy uses the optional `classifier` gem; if it is missing, compaction is skipped with a logged warning rather than raising.
749
797
 
750
798
  ## Learning Accumulation
751
799
 
@@ -763,18 +811,9 @@ reviewer.learn("This codebase prefers map/collect over manual array accumulation
763
811
  reviewer.run("Review snippet B") # learning is injected automatically
764
812
  ```
765
813
 
766
- Pass `learn: true` in the constructor to enable automatic end-of-session learning promotion via the `robot_lab-durable` gem:
767
-
768
- ```ruby
769
- reviewer = RobotLab.build(
770
- name: "reviewer",
771
- system_prompt: "You are a Ruby code reviewer.",
772
- learn: true,
773
- learn_domain: "ruby_review"
774
- )
775
- ```
814
+ Cross-session learning promotion is provided by the [robot_lab-durable](https://github.com/MadBomber/robot_lab-durable) gem, which hooks the `:learn` lifecycle family. See that gem's documentation for setup.
776
815
 
777
- Learnings deduplicate bidirectionally: if a broader learning is added that contains an existing narrower one, the narrower one is dropped. Learnings are persisted to the robot's `Memory` and survive a robot rebuild when the same `Memory` object is reused.
816
+ Learnings deduplicate bidirectionally: if a broader learning is added that contains an existing narrower one, the narrower one is dropped. Learnings are persisted to the robot's `Memory` for the lifetime of that robot.
778
817
 
779
818
  ```ruby
780
819
  reviewer.learnings # => ["This codebase prefers map/collect..."]
@@ -783,7 +822,9 @@ reviewer.learn("new fact") # deduplicates before storing
783
822
 
784
823
  ## Context Window Compression
785
824
 
786
- `robot.compress_history` prunes old conversation turns using TF-IDF cosine similarity, keeping only turns that are relevant to the most recent context. System messages and tool call/result pairs are always preserved.
825
+ `robot.compress_history` prunes old conversation turns using term-frequency cosine similarity, keeping only turns that are relevant to the most recent context. System messages and tool call/result pairs are always preserved.
826
+
827
+ Scoring uses raw term frequencies without IDF weighting — on a topic-focused conversation, IDF would suppress exactly the recurring terms that signal relevance.
787
828
 
788
829
  ```ruby
789
830
  # Basic compression: protect the 3 most recent turns, drop unrelated old turns
@@ -811,7 +852,7 @@ gem "classifier", "~> 2.3"
811
852
 
812
853
  ## Convergence Detection
813
854
 
814
- `RobotLab::Convergence` detects when two independent agents have reached the same conclusion using TF-IDF cosine similarity. Use it as a router fast-path to skip an expensive reconciler LLM call when verifiers already agree.
855
+ `RobotLab::Convergence` detects when two independent agents have reached the same conclusion using term-frequency cosine similarity (no IDF on a two-document comparison, IDF collapses the shared terms to zero). Use it to skip an expensive reconciler LLM call when verifiers already agree.
815
856
 
816
857
  ```ruby
817
858
  # Check similarity directly
@@ -826,18 +867,30 @@ RobotLab::Convergence.detected?(result_a.reply, result_b.reply)
826
867
  RobotLab::Convergence.detected?(text_a, text_b, threshold: 0.75)
827
868
  ```
828
869
 
829
- A common pattern is wiring convergence into a network router to skip reconciliation:
870
+ `similarity` returns `0.0` if either text is blank or shorter than 30 characters (`Convergence::MIN_TEXT_LENGTH`), since term-frequency scoring is not meaningful on very short strings. Short replies like `"yes"` will therefore never register as converged.
871
+
872
+ A common pattern is a gate robot that decides whether reconciliation is needed. Routing is done by overriding `call` and activating an `depends_on: :optional` task — networks have no separate router parameter:
830
873
 
831
874
  ```ruby
832
- router = ->(args) do
833
- a = args.context[:verifier_a]&.reply.to_s
834
- b = args.context[:verifier_b]&.reply.to_s
835
- RobotLab::Convergence.detected?(a, b) ? nil : ["reconciler"]
875
+ class ConvergenceGate < RobotLab::Robot
876
+ def call(result)
877
+ a = result.context[:verifier_a]&.reply.to_s
878
+ b = result.context[:verifier_b]&.reply.to_s
879
+
880
+ # Converged? Pass the result straight through and leave :reconciler dormant.
881
+ RobotLab::Convergence.detected?(a, b) ? result : result.activate(:reconciler)
882
+ end
836
883
  end
837
884
 
838
- network = RobotLab.create_network(name: "verify", router: router) do
839
- # ...
885
+ network = RobotLab.create_network(name: "verify") do
886
+ task :verifier_a, verifier_a, depends_on: :none
887
+ task :verifier_b, verifier_b, depends_on: :none
888
+ task :gate, ConvergenceGate.new(name: "gate"), depends_on: [:verifier_a, :verifier_b]
889
+ task :reconciler, reconciler, depends_on: :optional
840
890
  end
891
+
892
+ result = network.run(message: "Audit this change")
893
+ result.context.key?(:reconciler) # => false when the verifiers agreed
841
894
  ```
842
895
 
843
896
  Requires the `classifier` gem (`~> 2.3`).
@@ -943,11 +996,18 @@ RobotLab's optional capabilities are packaged as separate gems:
943
996
 
944
997
  | Gem | Description |
945
998
  |-----|-------------|
946
- | [robot_lab-ractor](https://github.com/MadBomber/robot_lab-ractor) | CPU parallelism via Ruby Ractors `ractor_safe` tools and DAG-scheduled parallel networks |
999
+ | [robot_lab-a2a](https://github.com/MadBomber/robot_lab-a2a) | Expose robots and networks as Agent2Agent (A2A) protocol services over HTTP+SSE |
1000
+ | [robot_lab-audit](https://github.com/MadBomber/robot_lab-audit) | SQLite-backed execution audit log wired through the Hook system |
1001
+ | [robot_lab-discovery](https://github.com/MadBomber/robot_lab-discovery) | Zero-configuration mDNS/DNS-SD robot discovery on local networks |
1002
+ | [robot_lab-document_store](https://github.com/MadBomber/robot_lab-document_store) | Embedding-based semantic document store for search / RAG |
1003
+ | [robot_lab-durable](https://github.com/MadBomber/robot_lab-durable) | HTM-backed long-term memory and cross-session knowledge persistence |
1004
+ | [robot_lab-ractor](https://github.com/MadBomber/robot_lab-ractor) | Ractor-based parallel tool execution and DAG-scheduled parallel networks |
947
1005
  | [robot_lab-rails](https://github.com/MadBomber/robot_lab-rails) | Rails Engine, generators, `RobotLab::Job` ActiveJob base with Turbo Stream broadcasting |
948
- | [robot_lab-durable](https://github.com/MadBomber/robot_lab-durable) | Cross-session knowledge persistence via YAML-backed durable store |
949
- | [robot_lab-document_store](https://github.com/MadBomber/robot_lab-document_store) | In-memory vector store with fastembed embeddings for semantic search / RAG |
950
- | [robot_lab-acp](https://github.com/MadBomber/robot_lab-acp) | Expose robots and networks as ACP (Agent Communication Protocol) HTTP+SSE services |
1006
+ | [robot_lab-to](https://github.com/MadBomber/robot_lab-to) | Autonomous overnight agent loop iterate a robot toward an objective |
1007
+ | [robot_lab-web](https://github.com/MadBomber/robot_lab-web) | Browser console stream a robot's run over Server-Sent Events |
1008
+
1009
+ > [!NOTE]
1010
+ > `robot_lab-acp` is retired; its functionality is superseded by `robot_lab-a2a`.
951
1011
 
952
1012
  ## Documentation
953
1013