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.
- checksums.yaml +4 -4
- data/README.md +124 -64
- data/docs/api/core/index.md +41 -15
- data/docs/api/core/memory.md +247 -29
- data/docs/api/core/network.md +255 -33
- data/docs/api/core/result.md +120 -32
- data/docs/api/core/robot.md +551 -61
- data/docs/api/core/state.md +87 -197
- data/docs/api/core/tool.md +165 -20
- data/docs/api/errors.md +110 -17
- data/docs/api/hooks.md +469 -0
- data/docs/api/index.md +80 -7
- data/docs/api/mcp/client.md +129 -35
- data/docs/api/mcp/index.md +164 -23
- data/docs/api/mcp/server.md +27 -3
- data/docs/api/mcp/transports.md +94 -22
- data/docs/api/messages/index.md +26 -3
- data/docs/api/messages/text-message.md +33 -11
- data/docs/api/messages/tool-call-message.md +27 -4
- data/docs/api/messages/tool-result-message.md +23 -4
- data/docs/api/messages/user-message.md +45 -8
- data/docs/api/skills.md +519 -0
- data/docs/api/streaming/context.md +28 -5
- data/docs/api/streaming/index.md +57 -11
- data/docs/api/support.md +846 -0
- data/docs/architecture/core-concepts.md +79 -31
- data/docs/architecture/index.md +86 -11
- data/docs/architecture/message-flow.md +66 -29
- data/docs/architecture/network-orchestration.md +145 -38
- data/docs/architecture/robot-execution.md +172 -90
- data/docs/architecture/state-management.md +31 -12
- data/docs/concepts.md +176 -21
- data/docs/examples/basic-chat.md +72 -19
- data/docs/examples/index.md +117 -31
- data/docs/examples/mcp-server.md +154 -45
- data/docs/examples/multi-robot-network.md +91 -21
- data/docs/examples/tool-usage.md +104 -37
- data/docs/getting-started/configuration.md +284 -97
- data/docs/getting-started/installation.md +53 -41
- data/docs/getting-started/quick-start.md +51 -6
- data/docs/guides/building-robots.md +258 -50
- data/docs/guides/creating-networks.md +214 -30
- data/docs/guides/hooks.md +141 -54
- data/docs/guides/knowledge.md +35 -4
- data/docs/guides/mcp-integration.md +211 -44
- data/docs/guides/memory.md +103 -12
- data/docs/guides/observability.md +95 -47
- data/docs/guides/streaming.md +184 -125
- data/docs/guides/using-tools.md +237 -17
- data/docs/index.md +36 -4
- data/examples/01_simple_robot.rb +2 -2
- data/examples/02_tools.rb +14 -4
- data/examples/03_network.rb +12 -7
- data/examples/04_mcp.rb +11 -4
- data/examples/05_streaming.rb +8 -5
- data/examples/06_prompt_templates.rb +13 -9
- data/examples/07_network_memory.rb +5 -5
- data/examples/08_llm_config.rb +20 -15
- data/examples/09_chaining.rb +4 -4
- data/examples/11_network_introspection.rb +4 -4
- data/examples/12_message_bus.rb +2 -2
- data/examples/13_spawn.rb +2 -2
- data/examples/14_rusty_circuit/README.md +1 -0
- data/examples/14_rusty_circuit/comic.rb +7 -3
- data/examples/14_rusty_circuit/display.rb +14 -0
- data/examples/14_rusty_circuit/heckler.rb +8 -6
- data/examples/14_rusty_circuit/open_mic.rb +17 -6
- data/examples/14_rusty_circuit/scout.rb +17 -10
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +14 -10
- data/examples/15_memory_network_and_bus/linux_writer.rb +2 -2
- data/examples/15_memory_network_and_bus/os_editor.rb +3 -1
- data/examples/15_memory_network_and_bus/os_writer.rb +4 -1
- data/examples/16_writers_room/writer.rb +22 -22
- data/examples/16_writers_room/writers_room.rb +2 -0
- data/examples/17_skills.rb +14 -13
- data/examples/18_rails/README.md +20 -1
- data/examples/18_rails/app/controllers/chat_controller.rb +5 -1
- data/examples/18_rails/app/jobs/robot_run_job.rb +11 -5
- data/examples/18_rails/app/robots/chat_robot.rb +11 -0
- data/examples/18_rails/config/initializers/robot_lab.rb +8 -0
- data/examples/19_token_tracking.rb +25 -9
- data/examples/20_circuit_breaker.rb +10 -7
- data/examples/21_learning_loop.rb +42 -16
- data/examples/22_context_compression.rb +23 -23
- data/examples/23_convergence.rb +24 -17
- data/examples/24_structured_delegation.rb +13 -8
- data/examples/25_history_search.rb +12 -8
- data/examples/27_incident_response/incident_response.rb +31 -13
- data/examples/28_mcp_discovery.rb +17 -13
- data/examples/29_ractor_tools.rb +4 -2
- data/examples/30_ractor_network.rb +22 -17
- data/examples/31_launch_assessment.rb +20 -9
- data/examples/32_newsletter_reader.rb +7 -2
- data/examples/33_stock_predictor.rb +34 -13
- data/examples/34_agentskills.rb +7 -3
- data/examples/35_hooks.rb +18 -8
- data/examples/README.md +199 -45
- data/examples/common.rb +79 -11
- data/examples/xyzzy.rb +8 -1
- data/lib/robot_lab/config.rb +10 -5
- data/lib/robot_lab/names.rb +402 -0
- data/lib/robot_lab/robot/agent_skill_matching.rb +1 -3
- data/lib/robot_lab/robot/bus_messaging.rb +16 -8
- data/lib/robot_lab/robot/template_rendering.rb +16 -3
- data/lib/robot_lab/robot.rb +23 -2
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +21 -15
- data/mkdocs.yml +6 -1
- metadata +7 -2
|
@@ -116,7 +116,10 @@ begin
|
|
|
116
116
|
rescue RobotLab::BudgetExceeded => e
|
|
117
117
|
puts e.message # "budget exceeded for cost: 0.51 > 0.5" — refused before spending
|
|
118
118
|
rescue RobotLab::InferenceError => e
|
|
119
|
-
puts e.message
|
|
119
|
+
puts e.message
|
|
120
|
+
# "Cost budget exceeded: $0.523100 used, budget is $0.500000" — this call pushed it over
|
|
121
|
+
# (the cost message is formatted with %.6f; the token message is not formatted:
|
|
122
|
+
# "Token budget exceeded: 10412 tokens used, budget is 10000")
|
|
120
123
|
end
|
|
121
124
|
```
|
|
122
125
|
|
|
@@ -143,12 +146,20 @@ robot = RobotLab.build(
|
|
|
143
146
|
local_tools: [StepTool],
|
|
144
147
|
max_tool_rounds: 10
|
|
145
148
|
)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
> [!WARNING]
|
|
152
|
+
> `run` defaults to `tools: :none`, so a plain `robot.run("...")` sends the LLM
|
|
153
|
+
> **no** tools — even when `local_tools:` were attached at build time — and the
|
|
154
|
+
> breaker can never fire. Pass `tools: :inherit` on the call to actually send the
|
|
155
|
+
> attached tools.
|
|
146
156
|
|
|
157
|
+
```ruby
|
|
147
158
|
begin
|
|
148
|
-
robot.run("Run all steps.")
|
|
159
|
+
robot.run("Run all steps.", tools: :inherit)
|
|
149
160
|
rescue RobotLab::ToolLoopError => e
|
|
150
|
-
puts
|
|
151
|
-
# => "Circuit breaker
|
|
161
|
+
puts e.message
|
|
162
|
+
# => "Circuit breaker triggered: 11 tool calls exceeded max_tool_rounds (10)"
|
|
152
163
|
end
|
|
153
164
|
```
|
|
154
165
|
|
|
@@ -170,6 +181,8 @@ Error: tool_use ids were found without tool_result blocks immediately after
|
|
|
170
181
|
Call `clear_messages` to flush the corrupted history before reusing the robot. The system prompt and all configuration (tools, `max_tool_rounds`, etc.) are preserved:
|
|
171
182
|
|
|
172
183
|
```ruby
|
|
184
|
+
begin
|
|
185
|
+
robot.run("Keep calling the tool.", tools: :inherit)
|
|
173
186
|
rescue RobotLab::ToolLoopError => e
|
|
174
187
|
puts "Breaker fired: #{e.message}"
|
|
175
188
|
end
|
|
@@ -191,7 +204,7 @@ unguarded = RobotLab.build(
|
|
|
191
204
|
system_prompt: "Use the provided tool to answer questions.",
|
|
192
205
|
local_tools: [DoubleTool]
|
|
193
206
|
)
|
|
194
|
-
result = unguarded.run("Double the number 21 using the tool.")
|
|
207
|
+
result = unguarded.run("Double the number 21 using the tool.", tools: :inherit)
|
|
195
208
|
puts result.reply # "The result is 42."
|
|
196
209
|
```
|
|
197
210
|
|
|
@@ -205,16 +218,18 @@ puts result.reply # "The result is 42."
|
|
|
205
218
|
|
|
206
219
|
### doom_loop_threshold
|
|
207
220
|
|
|
221
|
+
Doom loop detection is **always on** — the detector is installed unconditionally on every `run()`. `doom_loop_threshold:` does not enable it; it only tunes the number of repetitions after which it fires. The default is `3`.
|
|
222
|
+
|
|
208
223
|
```ruby
|
|
209
224
|
robot = RobotLab.build(
|
|
210
225
|
name: "runner",
|
|
211
226
|
system_prompt: "Execute all steps.",
|
|
212
227
|
local_tools: [StepTool],
|
|
213
|
-
doom_loop_threshold: 3
|
|
228
|
+
doom_loop_threshold: 5 # tune the always-on detector; default is 3
|
|
214
229
|
)
|
|
215
230
|
```
|
|
216
231
|
|
|
217
|
-
|
|
232
|
+
The detector catches two patterns:
|
|
218
233
|
|
|
219
234
|
- **Consecutive repetition** — `[A, A, A]` (same tool called N times in a row)
|
|
220
235
|
- **Cyclic repetition** — `[A, B, C, A, B, C, A, B, C]` (same sequence repeated N times)
|
|
@@ -252,27 +267,38 @@ Long-running robots accumulate conversation history. Eventually, the cumulative
|
|
|
252
267
|
|
|
253
268
|
### auto_compact
|
|
254
269
|
|
|
255
|
-
|
|
270
|
+
`auto_compact` and `compact_threshold` are **`RunConfig` fields only** — they are not constructor keyword arguments. Build a `RunConfig` and pass it as `config:`:
|
|
256
271
|
|
|
257
272
|
```ruby
|
|
258
273
|
# Compact when estimated token usage exceeds 80% of the model's context window
|
|
274
|
+
config = RobotLab::RunConfig.new(auto_compact: :context_window)
|
|
275
|
+
|
|
259
276
|
robot = RobotLab.build(
|
|
260
277
|
name: "analyst",
|
|
261
278
|
system_prompt: "You are a research analyst.",
|
|
262
|
-
|
|
279
|
+
config: config
|
|
263
280
|
)
|
|
264
281
|
```
|
|
265
282
|
|
|
283
|
+
> [!WARNING]
|
|
284
|
+
> `Robot#initialize` has a closed keyword list — it takes no `**rest`. Passing
|
|
285
|
+
> `auto_compact:` or `compact_threshold:` directly to `RobotLab.build` raises
|
|
286
|
+
> `ArgumentError: unknown keyword`.
|
|
287
|
+
|
|
266
288
|
### Tuning the Threshold
|
|
267
289
|
|
|
268
290
|
`compact_threshold:` sets the fraction of the model's context window that triggers compaction. Defaults to `0.80` (80%):
|
|
269
291
|
|
|
270
292
|
```ruby
|
|
293
|
+
config = RobotLab::RunConfig.new(
|
|
294
|
+
auto_compact: :context_window,
|
|
295
|
+
compact_threshold: 0.70 # compact earlier, at 70%
|
|
296
|
+
)
|
|
297
|
+
|
|
271
298
|
robot = RobotLab.build(
|
|
272
299
|
name: "analyst",
|
|
273
300
|
system_prompt: "You are a research analyst.",
|
|
274
|
-
|
|
275
|
-
compact_threshold: 0.70 # compact earlier, at 70%
|
|
301
|
+
config: config
|
|
276
302
|
)
|
|
277
303
|
```
|
|
278
304
|
|
|
@@ -281,16 +307,20 @@ robot = RobotLab.build(
|
|
|
281
307
|
Pass a `Proc` to take full control — the proc decides both when and how to compact:
|
|
282
308
|
|
|
283
309
|
```ruby
|
|
284
|
-
|
|
285
|
-
name: "analyst",
|
|
286
|
-
system_prompt: "You are a research analyst.",
|
|
310
|
+
config = RobotLab::RunConfig.new(
|
|
287
311
|
auto_compact: ->(r) {
|
|
288
312
|
r.compress_history(recent_turns: 5) if r.chat.messages.size > 40
|
|
289
313
|
}
|
|
290
314
|
)
|
|
315
|
+
|
|
316
|
+
robot = RobotLab.build(
|
|
317
|
+
name: "analyst",
|
|
318
|
+
system_prompt: "You are a research analyst.",
|
|
319
|
+
config: config
|
|
320
|
+
)
|
|
291
321
|
```
|
|
292
322
|
|
|
293
|
-
The proc receives the robot instance and is called
|
|
323
|
+
The proc receives the robot instance and is called once per `run()` when messages are non-empty.
|
|
294
324
|
|
|
295
325
|
### Options
|
|
296
326
|
|
|
@@ -300,13 +330,6 @@ The proc receives the robot instance and is called before every `run()` when mes
|
|
|
300
330
|
| `:context_window` | Compact when estimated token usage exceeds `compact_threshold` fraction of model's context window |
|
|
301
331
|
| `Proc` | Called with the robot; application decides when and how to compact |
|
|
302
332
|
|
|
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
333
|
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
334
|
|
|
312
335
|
---
|
|
@@ -387,20 +410,33 @@ end
|
|
|
387
410
|
|
|
388
411
|
After all three runs, `reviewer.learnings` contains up to three insights (fewer if any are subsets of others).
|
|
389
412
|
|
|
390
|
-
### Durable Learning (learn
|
|
413
|
+
### Durable Learning (the `:learn` hook family)
|
|
414
|
+
|
|
415
|
+
Core RobotLab keeps learnings for the life of the process only. Cross-session persistence is supplied by the [`robot_lab-durable`](https://github.com/MadBomber/robot_lab-durable) gem, which registers a `RobotLab::Hook` on the `:learn` family — there is no `learn:` constructor shorthand.
|
|
391
416
|
|
|
392
|
-
|
|
417
|
+
> [!WARNING]
|
|
418
|
+
> `learn:` and `learn_domain:` are **not** constructor keyword arguments and do
|
|
419
|
+
> not exist anywhere in the codebase. `RobotLab.build(learn: true)` raises
|
|
420
|
+
> `ArgumentError: unknown keyword: :learn`.
|
|
421
|
+
|
|
422
|
+
The wiring is the ordinary hook registration described in the [Hooks guide](hooks.md) — an `on_learn` handler receives each learning after session-level deduplication and decides whether to persist it:
|
|
393
423
|
|
|
394
424
|
```ruby
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
425
|
+
class DurableLearnHook < RobotLab::Hook
|
|
426
|
+
self.namespace = :durable
|
|
427
|
+
|
|
428
|
+
def self.on_learn(ctx)
|
|
429
|
+
return unless ctx.stored
|
|
430
|
+
|
|
431
|
+
DurableStore.promote(text: ctx.text, robot: ctx.robot.name, domain: ctx.local.domain)
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
reviewer = RobotLab.build(name: "reviewer", system_prompt: "You are a Ruby code reviewer.")
|
|
436
|
+
reviewer.on(DurableLearnHook, context: { domain: "ruby_review" })
|
|
401
437
|
```
|
|
402
438
|
|
|
403
|
-
|
|
439
|
+
The extension promotes durable insights to a YAML-backed store that persists across process restarts; see the gem's own README for its registration entry point.
|
|
404
440
|
|
|
405
441
|
### Memory Persistence
|
|
406
442
|
|
|
@@ -522,29 +558,41 @@ Texts shorter than 30 characters always return `0.0`.
|
|
|
522
558
|
| Same topic, different emphasis | 0.45 – 0.60 |
|
|
523
559
|
| Unrelated | < 0.15 |
|
|
524
560
|
|
|
525
|
-
###
|
|
561
|
+
### Reconciler Fast-Path Pattern
|
|
526
562
|
|
|
527
|
-
Skip the reconciler when verifiers agree:
|
|
563
|
+
Skip the reconciler when verifiers agree. RobotLab has no router object — routing is done by declaring the optional branch as a task with `depends_on: :optional` and having a preceding robot call `result.activate(:task_name)` on it. Subclass `RobotLab::Robot` and override `#call` to make the decision:
|
|
528
564
|
|
|
529
565
|
```ruby
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
566
|
+
class ConvergenceGate < RobotLab::Robot
|
|
567
|
+
def call(result)
|
|
568
|
+
# result.context is keyed by ROBOT name, not task name
|
|
569
|
+
a = result.context[:verifier_a]&.reply.to_s
|
|
570
|
+
b = result.context[:verifier_b]&.reply.to_s
|
|
533
571
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
["reconciler"] # diverged — send to reconciler
|
|
572
|
+
return result if RobotLab::Convergence.detected?(a, b) # agree — reconciler stays dormant
|
|
573
|
+
|
|
574
|
+
result.activate(:reconciler) # diverged — activate the branch
|
|
538
575
|
end
|
|
539
576
|
end
|
|
540
577
|
|
|
541
|
-
network = RobotLab.create_network(
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
578
|
+
network = RobotLab.create_network(name: "fact_check") do
|
|
579
|
+
task :verifier_a, verifier_a, depends_on: :none
|
|
580
|
+
task :verifier_b, verifier_b, depends_on: :none
|
|
581
|
+
task :gate, ConvergenceGate.new(name: "gate"), depends_on: %i[verifier_a verifier_b]
|
|
582
|
+
task :reconciler, reconciler, depends_on: :optional
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
result = network.run(message: "Is the deployment healthy?")
|
|
586
|
+
result.activated_steps # => [] when they agreed, [:reconciler] when they diverged
|
|
546
587
|
```
|
|
547
588
|
|
|
589
|
+
The gate robot never calls the LLM — overriding `#call` replaces the default "run and continue" behaviour entirely, so the decision costs nothing.
|
|
590
|
+
|
|
591
|
+
> [!NOTE]
|
|
592
|
+
> `result.context` is keyed by the **robot's** name (`@name`), not the task name.
|
|
593
|
+
> The lookups above work because each verifier's `name:` matches its task label.
|
|
594
|
+
> If they differ, index by the robot name.
|
|
595
|
+
|
|
548
596
|
Tune `threshold:` to control how strictly "agreement" is defined. A lower threshold (e.g., `0.6`) accepts more variation between verifiers; a higher threshold (e.g., `0.9`) only fast-paths near-identical responses.
|
|
549
597
|
|
|
550
598
|
### Optional Dependency
|
|
@@ -662,10 +710,10 @@ Once registered, a run prints a line per event:
|
|
|
662
710
|
```
|
|
663
711
|
· math_bot: thinking…
|
|
664
712
|
· → calculate operation="add"
|
|
665
|
-
·
|
|
713
|
+
· → calculate operation="multiply"
|
|
666
714
|
```
|
|
667
715
|
|
|
668
|
-
-
|
|
716
|
+
- Once per `run`: `"<robot name>: thinking…"` — Narrator hooks `before_llm_generation`, which fires exactly once per `robot.run`; the provider's tool loop happens *inside* that hook, so the line is not repeated per LLM API call
|
|
669
717
|
- Before each tool call: `"→ <tool name> <first arg>=<value>"` — only the *first* argument is shown, truncated to 80 characters
|
|
670
718
|
- After a tool call: `" ✗ <error message>"` — printed only when the tool raised; silent on success
|
|
671
719
|
|