robot_lab 0.2.6 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +124 -64
  3. data/docs/api/core/index.md +41 -15
  4. data/docs/api/core/memory.md +247 -29
  5. data/docs/api/core/network.md +255 -33
  6. data/docs/api/core/result.md +120 -32
  7. data/docs/api/core/robot.md +551 -61
  8. data/docs/api/core/state.md +87 -197
  9. data/docs/api/core/tool.md +165 -20
  10. data/docs/api/errors.md +110 -17
  11. data/docs/api/hooks.md +469 -0
  12. data/docs/api/index.md +80 -7
  13. data/docs/api/mcp/client.md +129 -35
  14. data/docs/api/mcp/index.md +164 -23
  15. data/docs/api/mcp/server.md +27 -3
  16. data/docs/api/mcp/transports.md +94 -22
  17. data/docs/api/messages/index.md +26 -3
  18. data/docs/api/messages/text-message.md +33 -11
  19. data/docs/api/messages/tool-call-message.md +27 -4
  20. data/docs/api/messages/tool-result-message.md +23 -4
  21. data/docs/api/messages/user-message.md +45 -8
  22. data/docs/api/skills.md +519 -0
  23. data/docs/api/streaming/context.md +28 -5
  24. data/docs/api/streaming/index.md +57 -11
  25. data/docs/api/support.md +846 -0
  26. data/docs/architecture/core-concepts.md +79 -31
  27. data/docs/architecture/index.md +86 -11
  28. data/docs/architecture/message-flow.md +66 -29
  29. data/docs/architecture/network-orchestration.md +145 -38
  30. data/docs/architecture/robot-execution.md +172 -90
  31. data/docs/architecture/state-management.md +31 -12
  32. data/docs/concepts.md +176 -21
  33. data/docs/examples/basic-chat.md +72 -19
  34. data/docs/examples/index.md +117 -31
  35. data/docs/examples/mcp-server.md +154 -45
  36. data/docs/examples/multi-robot-network.md +91 -21
  37. data/docs/examples/tool-usage.md +104 -37
  38. data/docs/getting-started/configuration.md +284 -97
  39. data/docs/getting-started/installation.md +53 -41
  40. data/docs/getting-started/quick-start.md +51 -6
  41. data/docs/guides/building-robots.md +258 -50
  42. data/docs/guides/creating-networks.md +214 -30
  43. data/docs/guides/hooks.md +141 -54
  44. data/docs/guides/knowledge.md +35 -4
  45. data/docs/guides/mcp-integration.md +211 -44
  46. data/docs/guides/memory.md +103 -12
  47. data/docs/guides/observability.md +95 -47
  48. data/docs/guides/streaming.md +184 -125
  49. data/docs/guides/using-tools.md +237 -17
  50. data/docs/index.md +36 -4
  51. data/examples/01_simple_robot.rb +2 -2
  52. data/examples/02_tools.rb +14 -4
  53. data/examples/03_network.rb +12 -7
  54. data/examples/04_mcp.rb +11 -4
  55. data/examples/05_streaming.rb +8 -5
  56. data/examples/06_prompt_templates.rb +13 -9
  57. data/examples/07_network_memory.rb +5 -5
  58. data/examples/08_llm_config.rb +20 -15
  59. data/examples/09_chaining.rb +4 -4
  60. data/examples/11_network_introspection.rb +4 -4
  61. data/examples/12_message_bus.rb +2 -2
  62. data/examples/13_spawn.rb +2 -2
  63. data/examples/14_rusty_circuit/README.md +1 -0
  64. data/examples/14_rusty_circuit/comic.rb +7 -3
  65. data/examples/14_rusty_circuit/display.rb +14 -0
  66. data/examples/14_rusty_circuit/heckler.rb +8 -6
  67. data/examples/14_rusty_circuit/open_mic.rb +17 -6
  68. data/examples/14_rusty_circuit/scout.rb +17 -10
  69. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +14 -10
  70. data/examples/15_memory_network_and_bus/linux_writer.rb +2 -2
  71. data/examples/15_memory_network_and_bus/os_editor.rb +3 -1
  72. data/examples/15_memory_network_and_bus/os_writer.rb +4 -1
  73. data/examples/16_writers_room/writer.rb +22 -22
  74. data/examples/16_writers_room/writers_room.rb +2 -0
  75. data/examples/17_skills.rb +14 -13
  76. data/examples/18_rails/README.md +20 -1
  77. data/examples/18_rails/app/controllers/chat_controller.rb +5 -1
  78. data/examples/18_rails/app/jobs/robot_run_job.rb +11 -5
  79. data/examples/18_rails/app/robots/chat_robot.rb +11 -0
  80. data/examples/18_rails/config/initializers/robot_lab.rb +8 -0
  81. data/examples/19_token_tracking.rb +25 -9
  82. data/examples/20_circuit_breaker.rb +10 -7
  83. data/examples/21_learning_loop.rb +42 -16
  84. data/examples/22_context_compression.rb +23 -23
  85. data/examples/23_convergence.rb +24 -17
  86. data/examples/24_structured_delegation.rb +13 -8
  87. data/examples/25_history_search.rb +12 -8
  88. data/examples/27_incident_response/incident_response.rb +31 -13
  89. data/examples/28_mcp_discovery.rb +17 -13
  90. data/examples/29_ractor_tools.rb +4 -2
  91. data/examples/30_ractor_network.rb +22 -17
  92. data/examples/31_launch_assessment.rb +20 -9
  93. data/examples/32_newsletter_reader.rb +7 -2
  94. data/examples/33_stock_predictor.rb +34 -13
  95. data/examples/34_agentskills.rb +7 -3
  96. data/examples/35_hooks.rb +18 -8
  97. data/examples/README.md +199 -45
  98. data/examples/common.rb +79 -11
  99. data/examples/xyzzy.rb +8 -1
  100. data/lib/robot_lab/config.rb +10 -5
  101. data/lib/robot_lab/names.rb +402 -0
  102. data/lib/robot_lab/robot/agent_skill_matching.rb +1 -3
  103. data/lib/robot_lab/robot/bus_messaging.rb +16 -8
  104. data/lib/robot_lab/robot/template_rendering.rb +16 -3
  105. data/lib/robot_lab/robot.rb +23 -2
  106. data/lib/robot_lab/version.rb +1 -1
  107. data/lib/robot_lab.rb +21 -15
  108. data/mkdocs.yml +6 -1
  109. metadata +7 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d401853bc5fff2646793cfff4fbda7082b18b0e07b833b3f4de6545fca574125
4
- data.tar.gz: f9964095406885bed329d8f2e7189cbb582e1c4665e3d53781b83371ee9016c7
3
+ metadata.gz: 36b770d963e004a951cdf35d07a2e867e4e661a9e342e8047184cfd9dafd6e15
4
+ data.tar.gz: f53bb5c52efcd6169bf36afc69306a9c03e9cfcabdaf5b0a94871802e2909f55
5
5
  SHA512:
6
- metadata.gz: dd29d885c9ea4e0b91bffe49b757808213e8011302fac48b4632c37d0cc95315a559f80240fabca92c0a309fead3f1f72b1da3ac7eccdb440059451fc53c5a0e
7
- data.tar.gz: 518c45909e25437da64e16ad69a5bfe5f361ddd602dc64c0ad475acde1c0b7946a0773fa36960a0baea3294ae7a7cef1835b6d08be2034e3a4a905fa74715eb2
6
+ metadata.gz: 850bde0e9ac1b7d65176bd067f42751d6d9db64517ca665d4765837fc5493e204f9b333cac08019cb451962d4d95789f986dee2ff08e55ae5e6a0d3d034f4f50
7
+ data.tar.gz: 97eb4dc66458a9a43f861e3f9c927eb6cfa63d33ddfff150b4181a45dc2c0db9570095f2e71d1182b76b4ee85e7086a6014b5536e805d9956f0860f9b4a03f50
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
 
@@ -10,16 +10,28 @@ classDiagram
10
10
  +name: String
11
11
  +description: String
12
12
  +model: String
13
- +template: String
14
- +tools: Array~Tool~
15
- +run(message) Message
13
+ +template: Symbol
14
+ +local_tools: Array~Tool~
15
+ +mcp_tools: Array~Tool~
16
+ +tools_config: Symbol_or_Array
17
+ +run(message) RobotResult
18
+ }
19
+
20
+ class RobotResult {
21
+ +robot_name: String
22
+ +reply: String
23
+ +output: Array~TextMessage~
24
+ +input_tokens: Integer
25
+ +output_tokens: Integer
26
+ +export() Hash
16
27
  }
17
28
 
18
29
  class Network {
19
30
  +name: String
20
31
  +robots: Hash
21
32
  +config: RunConfig
22
- +run(message)
33
+ +memory: Memory
34
+ +run(message) SimpleFlow_Result
23
35
  }
24
36
 
25
37
  class RunConfig {
@@ -32,8 +44,9 @@ classDiagram
32
44
  class Tool {
33
45
  +name: String
34
46
  +description: String
35
- +parameters: Hash
36
- +handler: Proc
47
+ +robot: Robot
48
+ +mcp: String
49
+ +call(args)
37
50
  }
38
51
 
39
52
  class Memory {
@@ -60,22 +73,28 @@ classDiagram
60
73
  Robot --> RunConfig : uses
61
74
  Robot --> Tool : has
62
75
  Robot --> Memory : uses
76
+ Robot ..> RobotResult : returns
63
77
  Network --> Memory : uses
64
78
  Robot ..> RobotMessage : sends/receives
79
+ Memory --> StateProxy : data
65
80
  ```
66
81
 
67
82
  ## Classes
68
83
 
69
84
  | Class | Purpose |
70
85
  |-------|---------|
71
- | [Robot](robot.md) | LLM agent with personality, tools, and model configuration |
72
- | [Network](network.md) | Container for robots with routing and orchestration |
73
- | RunConfig | Shared configuration for LLM, tools, callbacks, and infrastructure |
74
- | [Tool](tool.md) | Callable function with parameters and handler |
86
+ | [Robot](robot.md) | LLM agent with templates, tools, memory, and model configuration |
87
+ | [RobotResult](result.md) | Value object returned by `Robot#run` |
88
+ | [Network](network.md) | Container for robots with DAG orchestration |
89
+ | [RunConfig](robot.md#runconfig) | Shared configuration for LLM, tools, callbacks, and infrastructure |
90
+ | [Tool](tool.md) | Callable function with parameters and an `execute` method |
75
91
  | [AskUser](tool.md#built-in-askuser) | Built-in tool for terminal-based user interaction |
76
92
  | [Memory](memory.md) | Reactive key-value store for sharing data |
93
+ | [StateProxy](state.md) | Hash/method-access wrapper returned by `memory.data` |
77
94
  | RobotMessage | Typed envelope for bus-based inter-robot communication |
78
- | `RobotLab::Runnable` | Shared interface (`crew`, `chief`, `run`) implemented by both `Robot` and `Network` — see [Runnable Protocol](../../architecture/core-concepts.md#runnable-protocol) |
95
+ | `RobotLab::Runnable` | Shared interface (`crew`, `chief`, `robot_count`, `network?`, `single?`) implemented by both `Robot` and `Network` — see [Runnable Protocol](../../architecture/core-concepts.md#runnable-protocol) |
96
+
97
+ There is no `RobotLab::State` class and no `RobotLab::NetworkRun` class.
79
98
 
80
99
  ## Quick Examples
81
100
 
@@ -89,18 +108,25 @@ robot = RobotLab.build(
89
108
  local_tools: [greet_tool]
90
109
  )
91
110
 
92
- result = robot.run("Hello!")
111
+ result = robot.run("Hello!") # no tools sent — run() defaults to tools: :none
112
+ result = robot.run("Hello!", tools: :inherit) # sends greet_tool
113
+ result.reply # => String
93
114
  ```
94
115
 
116
+ `Robot#run` returns a [`RobotLab::RobotResult`](result.md), not a `Message`.
117
+
95
118
  ### Network
96
119
 
120
+ The builder DSL method is `task` (there is no `step`):
121
+
97
122
  ```ruby
98
123
  network = RobotLab.create_network(name: "my_network") do
99
- step :analyzer, analyzer_robot, depends_on: :none
100
- step :writer, writer_robot, depends_on: [:analyzer]
124
+ task :analyzer, analyzer_robot, depends_on: :none
125
+ task :writer, writer_robot, depends_on: [:analyzer]
101
126
  end
102
127
 
103
- result = network.run(message: "Process this")
128
+ result = network.run(message: "Process this") # => SimpleFlow::Result
129
+ result.value # => RobotResult of the last task
104
130
  ```
105
131
 
106
132
  ### Memory