robot_lab 0.2.7 → 0.3.0
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/.envrc +6 -2
- data/.loki +5 -0
- data/Archspec.rb +44 -0
- data/CHANGELOG.md +70 -1
- data/CLAUDE.md +4 -2
- data/README.md +15 -17
- data/Rakefile +6 -111
- data/_typos.toml +21 -0
- data/docs/api/index.md +2 -2
- data/docs/api/skills.md +53 -149
- data/docs/api/support.md +1 -1
- data/docs/architecture/core-concepts.md +1 -1
- data/docs/architecture/index.md +5 -4
- data/docs/architecture/state-management.md +4 -4
- data/docs/concepts.md +1 -1
- data/docs/getting-started/configuration.md +4 -1
- data/docs/getting-started/installation.md +1 -4
- data/docs/guides/hooks.md +122 -0
- data/docs/guides/memory.md +2 -2
- data/docs/guides/using-tools.md +10 -1
- data/examples/.envrc +2 -0
- data/examples/02_tools.rb +8 -8
- data/examples/03_network.rb +1 -1
- data/examples/04_mcp.rb +7 -5
- data/examples/08_llm_config.rb +5 -5
- data/examples/09_chaining.rb +3 -3
- data/examples/14_rusty_circuit/comic.rb +8 -8
- data/examples/14_rusty_circuit/scout.rb +4 -4
- data/examples/15_memory_network_and_bus/README.md +66 -0
- data/examples/15_memory_network_and_bus/output/combined_article.md +5 -7
- data/examples/15_memory_network_and_bus/output/final_article.md +5 -10
- data/examples/15_memory_network_and_bus/output/linux_draft.md +3 -3
- data/examples/15_memory_network_and_bus/output/mac_draft.md +3 -3
- data/examples/15_memory_network_and_bus/output/memory.json +6 -6
- data/examples/15_memory_network_and_bus/output/revision_1.md +21 -10
- data/examples/15_memory_network_and_bus/output/revision_2.md +43 -6
- data/examples/15_memory_network_and_bus/output/revision_3.md +8 -0
- data/examples/15_memory_network_and_bus/output/windows_draft.md +3 -3
- data/examples/16_writers_room/tools.rb +14 -14
- data/examples/19_token_tracking.rb +2 -2
- data/examples/20_circuit_breaker.rb +3 -3
- data/examples/22_context_compression.rb +1 -1
- data/examples/27_incident_response/README.md +65 -0
- data/examples/28_mcp_discovery.rb +2 -2
- data/examples/29_ractor_tools.rb +4 -4
- data/examples/30_ractor_network.rb +2 -2
- data/examples/33_stock_predictor.rb +8 -8
- data/examples/35_hooks.rb +3 -3
- data/examples/README.md +17 -0
- data/examples/common.rb +55 -23
- data/examples/run_all.rb +60 -0
- data/lib/robot_lab/agent_skill_catalog.rb +1 -0
- data/lib/robot_lab/ask_user.rb +5 -3
- data/lib/robot_lab/bus_poller.rb +2 -0
- data/lib/robot_lab/capabilities.rb +4 -0
- data/lib/robot_lab/config/defaults.yml +5 -5
- data/lib/robot_lab/config.rb +10 -2
- data/lib/robot_lab/doom_loop_detector.rb +6 -3
- data/lib/robot_lab/history_compressor.rb +5 -0
- data/lib/robot_lab/hook.rb +1 -0
- data/lib/robot_lab/hook_context.rb +4 -0
- data/lib/robot_lab/hook_registry.rb +1 -0
- data/lib/robot_lab/hooks.rb +6 -3
- data/lib/robot_lab/mcp/client.rb +2 -2
- data/lib/robot_lab/mcp/connection_poller.rb +16 -8
- data/lib/robot_lab/mcp/server_discovery.rb +1 -0
- data/lib/robot_lab/mcp/transports/sse.rb +3 -0
- data/lib/robot_lab/mcp/transports/stdio.rb +5 -0
- data/lib/robot_lab/mcp/transports/streamable_http.rb +5 -3
- data/lib/robot_lab/mcp/transports/websocket.rb +3 -0
- data/lib/robot_lab/memory.rb +39 -13
- data/lib/robot_lab/memory_change.rb +1 -0
- data/lib/robot_lab/message.rb +3 -0
- data/lib/robot_lab/names.rb +2 -4
- data/lib/robot_lab/network.rb +13 -6
- data/lib/robot_lab/robot/agent_skill_matching.rb +2 -0
- data/lib/robot_lab/robot/bus_messaging.rb +3 -0
- data/lib/robot_lab/robot/history_search.rb +2 -0
- data/lib/robot_lab/robot/hooking.rb +29 -0
- data/lib/robot_lab/robot/mcp_management.rb +11 -3
- data/lib/robot_lab/robot/result_building.rb +119 -0
- data/lib/robot_lab/robot/template_rendering.rb +13 -4
- data/lib/robot_lab/robot.rb +88 -130
- data/lib/robot_lab/robot_result.rb +3 -0
- data/lib/robot_lab/run_config.rb +57 -20
- data/lib/robot_lab/script_tool.rb +20 -39
- data/lib/robot_lab/state_proxy.rb +1 -0
- data/lib/robot_lab/streaming/context.rb +1 -0
- data/lib/robot_lab/streaming/events.rb +1 -0
- data/lib/robot_lab/task.rb +2 -0
- data/lib/robot_lab/tool.rb +11 -12
- data/lib/robot_lab/user_message.rb +1 -0
- data/lib/robot_lab/utils.rb +2 -0
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab/waiter.rb +3 -0
- data/lib/robot_lab.rb +9 -6
- metadata +23 -32
- data/lib/robot_lab/sandbox/null.rb +0 -13
- data/lib/robot_lab/sandbox/seatbelt.rb +0 -104
- data/lib/robot_lab/sandbox.rb +0 -52
data/docs/api/skills.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# Skills API
|
|
2
2
|
|
|
3
3
|
Class-level reference for the AgentSkills subsystem: skill bundles, the scripts
|
|
4
|
-
they expose as tools, the capabilities those scripts declare
|
|
5
|
-
|
|
4
|
+
they expose as tools, and the capabilities those scripts declare. Core itself
|
|
5
|
+
has **no sandboxing and no execution limitations** — every script runs
|
|
6
|
+
unconfined until the optional
|
|
7
|
+
[`robot_lab-sandbox`](https://github.com/MadBomber/robot_lab-sandbox) gem is
|
|
8
|
+
required, at which point it installs a confinement strategy (see
|
|
9
|
+
[ScriptTool.executor](#scripttoolexecutor) below). For the how-to, see
|
|
6
10
|
[Using Tools: Skill Scripts and Sandboxing](../guides/using-tools.md#skill-scripts-and-sandboxing)
|
|
7
11
|
and [Building Robots: Composable Skills](../guides/building-robots.md#composable-skills).
|
|
8
12
|
|
|
@@ -27,12 +31,16 @@ graph LR
|
|
|
27
31
|
G[effective grant<br/>declared ∩ ceiling]
|
|
28
32
|
end
|
|
29
33
|
|
|
30
|
-
subgraph "Execution"
|
|
34
|
+
subgraph "Execution (core)"
|
|
31
35
|
T[ScriptTool.from_path<br/>-> RobotLab::Tool]
|
|
36
|
+
EX{ScriptTool.executor<br/>set?}
|
|
37
|
+
UN[Open3.capture2e<br/>unconfined, no timeout]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
subgraph "robot_lab-sandbox (optional gem)"
|
|
32
41
|
SB{Sandbox.enabled?}
|
|
33
42
|
SE[Sandbox::Seatbelt<br/>macOS]
|
|
34
43
|
NU[Sandbox::Null<br/>passthrough]
|
|
35
|
-
P[script process]
|
|
36
44
|
end
|
|
37
45
|
|
|
38
46
|
R[Robot<br/>AgentSkillMatching] --> C
|
|
@@ -41,12 +49,12 @@ graph LR
|
|
|
41
49
|
S --> T
|
|
42
50
|
D --> G
|
|
43
51
|
CE --> G
|
|
44
|
-
T -->
|
|
52
|
+
T --> EX
|
|
53
|
+
EX -- "nil (default)" --> UN
|
|
54
|
+
EX -- "installed" --> SB
|
|
45
55
|
SB -- "off, or trust: core,<br/>or non-macOS" --> NU
|
|
46
56
|
SB -- "on + macOS" --> SE
|
|
47
57
|
G --> SE
|
|
48
|
-
SE --> P
|
|
49
|
-
NU --> P
|
|
50
58
|
```
|
|
51
59
|
|
|
52
60
|
---
|
|
@@ -264,9 +272,9 @@ of the ceiling, and `intersect` keeps the declared value.
|
|
|
264
272
|
capabilities.core? # => trust == "core"
|
|
265
273
|
```
|
|
266
274
|
|
|
267
|
-
A `core` skill is exempt from confinement: `
|
|
268
|
-
|
|
269
|
-
Reserve `trust: core` for bundles you wrote and audited.
|
|
275
|
+
A `core` skill is exempt from confinement: when `robot_lab-sandbox` is loaded,
|
|
276
|
+
`Sandbox.for` returns a `Null` strategy for it regardless of platform or
|
|
277
|
+
config. Reserve `trust: core` for bundles you wrote and audited.
|
|
270
278
|
|
|
271
279
|
### intersect
|
|
272
280
|
|
|
@@ -314,43 +322,32 @@ The generated tool takes a single optional `args` string parameter, which is
|
|
|
314
322
|
[`derive_name`](#scripttoolderive_name) and its description from
|
|
315
323
|
[`extract_description`](#scripttoolextract_description).
|
|
316
324
|
|
|
317
|
-
### ScriptTool.
|
|
325
|
+
### ScriptTool.executor
|
|
318
326
|
|
|
319
327
|
```ruby
|
|
320
|
-
RobotLab::ScriptTool.
|
|
328
|
+
RobotLab::ScriptTool.executor # => #call, or nil (the default)
|
|
329
|
+
RobotLab::ScriptTool.executor = obj # any object responding to
|
|
330
|
+
# call(cmd, capabilities:, skill_dir:)
|
|
321
331
|
```
|
|
322
332
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
- **Sandboxing on** — intersects `capabilities` with `Capabilities.ceiling`, wraps
|
|
328
|
-
the command with the strategy from `Sandbox.for`, runs it under the grant's
|
|
329
|
-
timeout, and cleans the strategy up in an `ensure`.
|
|
330
|
-
|
|
331
|
-
The declared `timeout:` therefore only takes effect when sandboxing is enabled.
|
|
333
|
+
The extension point core exposes for confinement. `nil` by default — core has
|
|
334
|
+
no sandboxing of its own. `robot_lab-sandbox`, when required, sets this to
|
|
335
|
+
`RobotLab::Sandbox::Executor`, which handles `capabilities`/timeout/cleanup
|
|
336
|
+
itself. See that gem's docs for what it does when installed.
|
|
332
337
|
|
|
333
|
-
### ScriptTool.
|
|
338
|
+
### ScriptTool.execute
|
|
334
339
|
|
|
335
340
|
```ruby
|
|
336
|
-
RobotLab::ScriptTool.
|
|
337
|
-
# => [String, Process::Status | nil]
|
|
341
|
+
RobotLab::ScriptTool.execute(cmd, capabilities:, skill_dir:) # => String
|
|
338
342
|
```
|
|
339
343
|
|
|
340
|
-
Run
|
|
341
|
-
|
|
342
|
-
`["<partial output>\n[killed: exceeded <N>s]", nil]` — a `nil` status is the
|
|
343
|
-
timeout signal.
|
|
344
|
-
|
|
345
|
-
### ScriptTool.terminate
|
|
346
|
-
|
|
347
|
-
```ruby
|
|
348
|
-
RobotLab::ScriptTool.terminate(pid)
|
|
349
|
-
```
|
|
344
|
+
Run a command array and return its combined stdout+stderr, or an error string.
|
|
345
|
+
Two paths:
|
|
350
346
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
347
|
+
- **`executor` is `nil`** (the default) — `Open3.capture2e`, unconfined, **no timeout**.
|
|
348
|
+
- **`executor` is set** — delegates entirely to `executor.call(cmd, capabilities:, skill_dir:)`.
|
|
349
|
+
Core no longer knows or cares what the executor does with `capabilities` or
|
|
350
|
+
how (or whether) it bounds execution time.
|
|
354
351
|
|
|
355
352
|
### ScriptTool.format_result
|
|
356
353
|
|
|
@@ -393,121 +390,27 @@ cannot be read.
|
|
|
393
390
|
|
|
394
391
|
---
|
|
395
392
|
|
|
396
|
-
##
|
|
393
|
+
## Confinement: robot_lab-sandbox
|
|
397
394
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
RobotLab::Sandbox.enabled? # => Boolean
|
|
404
|
-
RobotLab::Sandbox.enabled?(some_config)
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
`true` only when the config responds to `sandbox`, that section exists, and
|
|
408
|
-
`sandbox.enabled == true`. **Sandboxing is off by default** — see the
|
|
409
|
-
[`sandbox:` config section](../getting-started/configuration.md#skill-script-sandboxing-sandbox-section).
|
|
410
|
-
|
|
411
|
-
### Sandbox.macos?
|
|
412
|
-
|
|
413
|
-
```ruby
|
|
414
|
-
RobotLab::Sandbox.macos? # => RUBY_PLATFORM.include?("darwin")
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
### Sandbox.for
|
|
418
|
-
|
|
419
|
-
```ruby
|
|
420
|
-
strategy = RobotLab::Sandbox.for(grant, skill_dir:, macos: macos?)
|
|
421
|
-
# => Sandbox::Seatbelt or Sandbox::Null
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
| Name | Type | Default | Description |
|
|
425
|
-
|------|------|---------|-------------|
|
|
426
|
-
| `grant` | `Capabilities` | **required** | The **already-intersected** effective grant |
|
|
427
|
-
| `skill_dir` | `String` | **required** | Bundle root, always granted read access |
|
|
428
|
-
| `macos` | `Boolean` | `macos?` | Injectable so both branches are testable on any host |
|
|
395
|
+
`RobotLab::Sandbox`, `RobotLab::Sandbox::Seatbelt`, and `RobotLab::Sandbox::Null`
|
|
396
|
+
used to live here; they now ship in the separate
|
|
397
|
+
[`robot_lab-sandbox`](https://github.com/MadBomber/robot_lab-sandbox) gem, which
|
|
398
|
+
core has no dependency on. Requiring it installs `RobotLab::Sandbox::Executor`
|
|
399
|
+
as [`ScriptTool.executor`](#scripttoolexecutor):
|
|
429
400
|
|
|
430
|
-
|
|
431
|
-
`
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
many scripts does not flood the log.
|
|
442
|
-
|
|
443
|
-
---
|
|
444
|
-
|
|
445
|
-
## RobotLab::Sandbox::Null
|
|
446
|
-
|
|
447
|
-
Passthrough strategy — used off macOS and for `trust: core` skills.
|
|
448
|
-
|
|
449
|
-
| Method | Returns | Description |
|
|
450
|
-
|--------|---------|-------------|
|
|
451
|
-
| `wrap(cmd)` | `cmd` | Unchanged |
|
|
452
|
-
| `cleanup` | `nil` | No-op |
|
|
453
|
-
|
|
454
|
-
---
|
|
455
|
-
|
|
456
|
-
## RobotLab::Sandbox::Seatbelt
|
|
457
|
-
|
|
458
|
-
macOS strategy: generates a deny-by-default `sandbox-exec` profile from the grant
|
|
459
|
-
and wraps the command as `sandbox-exec -f <profile> <cmd...>`.
|
|
460
|
-
|
|
461
|
-
### Constants
|
|
462
|
-
|
|
463
|
-
| Constant | Description |
|
|
464
|
-
|----------|-------------|
|
|
465
|
-
| `SYSTEM_READ` | `/usr /bin /sbin /System /Library /opt /private/etc /dev /var/select` — the locations an interpreter needs to boot |
|
|
466
|
-
| `DEV_WRITE` | `/dev/null /dev/stdout /dev/stderr /dev/dtracehelper /dev/tty` — always writable |
|
|
467
|
-
|
|
468
|
-
### Constructor
|
|
469
|
-
|
|
470
|
-
```ruby
|
|
471
|
-
RobotLab::Sandbox::Seatbelt.new(grant, skill_dir:)
|
|
472
|
-
```
|
|
473
|
-
|
|
474
|
-
### wrap / cleanup
|
|
475
|
-
|
|
476
|
-
```ruby
|
|
477
|
-
cmd = strategy.wrap(["bash", "script.sh"])
|
|
478
|
-
# => ["sandbox-exec", "-f", "/tmp/robot_lab-sandbox-xxxx.sb", "bash", "script.sh"]
|
|
479
|
-
strategy.cleanup # unlinks the generated profile
|
|
480
|
-
```
|
|
481
|
-
|
|
482
|
-
`wrap` writes the profile to a `Tempfile`; `cleanup` unlinks it and swallows any
|
|
483
|
-
error (an already-removed file is fine). `ScriptTool.execute` always calls
|
|
484
|
-
`cleanup` in an `ensure`.
|
|
485
|
-
|
|
486
|
-
### profile_text
|
|
487
|
-
|
|
488
|
-
```ruby
|
|
489
|
-
strategy.profile_text # => String
|
|
490
|
-
```
|
|
401
|
+
- `Sandbox.enabled?` — reads `config.sandbox.enabled` (default `false`).
|
|
402
|
+
- `Sandbox.for(grant, skill_dir:, macos: macos?)` — picks `Sandbox::Seatbelt` on
|
|
403
|
+
macOS, `Sandbox::Null` (passthrough) elsewhere or for `trust: core` grants.
|
|
404
|
+
- `Sandbox::Seatbelt` — generates a deny-by-default `sandbox-exec` profile from
|
|
405
|
+
the effective grant (`fs_read`/`fs_write`/`network`), wrapping the command as
|
|
406
|
+
`sandbox-exec -f <profile> <cmd...>`. `$HOME` is never implicitly readable.
|
|
407
|
+
- `Sandbox::Null` — `wrap(cmd)` returns `cmd` unchanged; `cleanup` is a no-op.
|
|
408
|
+
- `Sandbox::Executor` — the piece that plugs into core: intersects the skill's
|
|
409
|
+
`Capabilities` with `Capabilities.ceiling`, wraps and runs the command under
|
|
410
|
+
the chosen strategy, and bounds it with the grant's `timeout`
|
|
411
|
+
(`Process.kill('-TERM', ...)` on the process group on expiry).
|
|
491
412
|
|
|
492
|
-
|
|
493
|
-
rather than inferred from behavior. It imports `bsd.sb` (without which a
|
|
494
|
-
deny-default profile aborts the binary before it starts), denies by default, then
|
|
495
|
-
allows: `process-fork`, `process-exec`, `sysctl-read`, `mach-lookup`,
|
|
496
|
-
`file-read-metadata` on any path, `file-read*` on `SYSTEM_READ` + the skill
|
|
497
|
-
directory + granted `fs_read` paths, `file-write*` on `DEV_WRITE` + granted
|
|
498
|
-
`fs_write` paths, and `network*` only when the grant allows it.
|
|
499
|
-
|
|
500
|
-
Every path is canonicalized to its symlink-free real path first, because macOS
|
|
501
|
-
symlinks `/tmp` → `/private/tmp` and the kernel matches against the real path. For
|
|
502
|
-
write targets that do not exist yet, the nearest existing ancestor is resolved and
|
|
503
|
-
the remainder re-appended.
|
|
504
|
-
|
|
505
|
-
!!! warning "`$HOME` is never implicitly readable"
|
|
506
|
-
Which is the point — SSH keys and cloud credentials stay out of reach. But it
|
|
507
|
-
also means an interpreter installed under `$HOME` (rbenv, asdf, mise, a
|
|
508
|
-
Homebrew prefix in `~`) is **invisible** to the sandboxed process and the
|
|
509
|
-
script fails to start. Grant that path explicitly in `fs_read`, or mark the
|
|
510
|
-
skill `trust: core`.
|
|
413
|
+
Full reference lives in that gem's own docs.
|
|
511
414
|
|
|
512
415
|
---
|
|
513
416
|
|
|
@@ -515,5 +418,6 @@ the remainder re-appended.
|
|
|
515
418
|
|
|
516
419
|
- [Using Tools: Skill Scripts and Sandboxing](../guides/using-tools.md#skill-scripts-and-sandboxing)
|
|
517
420
|
- [Configuration: `sandbox:` section](../getting-started/configuration.md#skill-script-sandboxing-sandbox-section)
|
|
421
|
+
- [robot_lab-sandbox](https://github.com/MadBomber/robot_lab-sandbox) — the optional confinement gem
|
|
518
422
|
- [Robot: Skills](core/robot.md#skills) — template skills, the other meaning
|
|
519
423
|
- [Tool](core/tool.md) — `Tool.create`, which `ScriptTool` builds on
|
data/docs/api/support.md
CHANGED
|
@@ -842,5 +842,5 @@ Thread-safe monotonic counter for event ordering.
|
|
|
842
842
|
|
|
843
843
|
- [Core Classes](core/index.md) — `Robot`, `Network`, `Memory`, `Tool`, `RobotResult`
|
|
844
844
|
- [Hooks API](hooks.md) — the extension seam these classes are wired into
|
|
845
|
-
- [Skills API](skills.md) — `AgentSkill`, `Capabilities`, `ScriptTool
|
|
845
|
+
- [Skills API](skills.md) — `AgentSkill`, `Capabilities`, `ScriptTool` (confinement lives in the optional `robot_lab-sandbox` gem)
|
|
846
846
|
- [Errors](errors.md) — including `Errors.retryable?` and `Errors.retryable_classes`
|
|
@@ -226,7 +226,7 @@ end
|
|
|
226
226
|
| `:results` | `Array` | Accumulated robot results |
|
|
227
227
|
| `:messages` | `Array` | Conversation history |
|
|
228
228
|
| `:session_id` | `String` | Session identifier |
|
|
229
|
-
| `:cache` | `Module` | Semantic cache (RubyLLM::SemanticCache) |
|
|
229
|
+
| `:cache` | `Module`, `nil` | Semantic cache (RubyLLM::SemanticCache when the optional gem is installed) |
|
|
230
230
|
|
|
231
231
|
### Reactive Features
|
|
232
232
|
|
data/docs/architecture/index.md
CHANGED
|
@@ -62,7 +62,7 @@ graph TB
|
|
|
62
62
|
H[MCP Client]
|
|
63
63
|
I[Tools < RubyLLM::Tool]
|
|
64
64
|
J[Templates / prompt_manager]
|
|
65
|
-
SK[AgentSkills
|
|
65
|
+
SK[AgentSkills<br/>+ optional Sandbox]
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
subgraph "Execution Layer"
|
|
@@ -138,8 +138,9 @@ graph LR
|
|
|
138
138
|
AS --> CAT[AgentSkillCatalog]
|
|
139
139
|
AS --> CAP[Capabilities]
|
|
140
140
|
AS --> ST[ScriptTool]
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
ST --> EX{ScriptTool.executor}
|
|
142
|
+
EX -. "installed by<br/>robot_lab-sandbox" .-> SB[Sandbox<br/>Seatbelt or Null]
|
|
143
|
+
CAP -. "used if installed" .-> SB
|
|
143
144
|
|
|
144
145
|
HC --> TA[TextAnalysis<br/>TF / TF-IDF]
|
|
145
146
|
HS --> TA
|
|
@@ -165,7 +166,7 @@ dependency.
|
|
|
165
166
|
| **Config** | MywayConfig-based global configuration with env var and file support | [Configuration](#configuration) |
|
|
166
167
|
| **Hook** | Handler base class for the seven hook families — the framework's extension seam | [Hooks API](../api/hooks.md) |
|
|
167
168
|
| **AgentSkill** | A `SKILL.md` bundle whose instructions and `scripts/` become prompt text and tools | [Skills API](../api/skills.md) |
|
|
168
|
-
| **Sandbox** | Opt-in OS-level confinement (macOS Seatbelt) for skill scripts, derived from `Capabilities` | [Skills API](../api/skills.md) |
|
|
169
|
+
| **Sandbox** | Opt-in OS-level confinement (macOS Seatbelt) for skill scripts, derived from `Capabilities`; ships in the optional `robot_lab-sandbox` gem, not core | [Skills API](../api/skills.md) |
|
|
169
170
|
| **RobotMessage** | Immutable envelope for TypedBus inter-robot messaging, serialized per robot by `BusPoller` | [Support API](../api/support.md) |
|
|
170
171
|
| **Budget::Ledger** | Thread-safe reserve/reconcile ledger behind `token_budget` / `cost_budget` | [Support API](../api/support.md) |
|
|
171
172
|
|
|
@@ -13,7 +13,7 @@ memory.data # StateProxy - custom key-value data with method-style access
|
|
|
13
13
|
memory.results # Array<RobotResult> - execution history
|
|
14
14
|
memory.messages # Array<Message> - conversation history
|
|
15
15
|
memory.session_id # String - optional persistence identifier
|
|
16
|
-
memory.cache # RubyLLM::SemanticCache
|
|
16
|
+
memory.cache # RubyLLM::SemanticCache when the optional gem is installed, else nil
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
## Standalone Robot Memory
|
|
@@ -108,7 +108,7 @@ Memory has five reserved keys with special behavior and dedicated accessors:
|
|
|
108
108
|
| `:results` | `Array<RobotResult>` | Accumulated robot execution results |
|
|
109
109
|
| `:messages` | `Array<Message>` | Conversation history |
|
|
110
110
|
| `:session_id` | `String` | Conversation session identifier |
|
|
111
|
-
| `:cache` | `RubyLLM::SemanticCache` | Semantic cache module (read-only after init) |
|
|
111
|
+
| `:cache` | `RubyLLM::SemanticCache` or `nil` | Semantic cache module when the optional ruby_llm-semantic_cache gem is installed (read-only after init) |
|
|
112
112
|
|
|
113
113
|
Reserved keys are accessed through dedicated methods and are excluded from `memory.keys`:
|
|
114
114
|
|
|
@@ -118,7 +118,7 @@ memory.data.category #=> "billing" (method-style via StateProxy)
|
|
|
118
118
|
|
|
119
119
|
memory.results #=> []
|
|
120
120
|
memory.session_id #=> nil
|
|
121
|
-
memory.cache #=> RubyLLM::SemanticCache (the module itself), or nil when enable_cache: false
|
|
121
|
+
memory.cache #=> RubyLLM::SemanticCache (the module itself), or nil when enable_cache: false or the optional gem is absent
|
|
122
122
|
```
|
|
123
123
|
|
|
124
124
|
## StateProxy
|
|
@@ -319,7 +319,7 @@ memory = Memory.from_hash(hash)
|
|
|
319
319
|
|
|
320
320
|
## Semantic Cache
|
|
321
321
|
|
|
322
|
-
Memory includes a semantic cache via `RubyLLM::SemanticCache` that reduces costs and latency by returning cached responses for semantically equivalent queries:
|
|
322
|
+
Memory includes a semantic cache via `RubyLLM::SemanticCache` (optional ruby_llm-semantic_cache gem; not yet ruby_llm 2.0-compatible, so `memory.cache` is `nil` without it) that reduces costs and latency by returning cached responses for semantically equivalent queries:
|
|
323
323
|
|
|
324
324
|
```ruby
|
|
325
325
|
# Using the cache with fetch
|
data/docs/concepts.md
CHANGED
|
@@ -338,7 +338,7 @@ robot.clear_messages(keep_system: true)
|
|
|
338
338
|
| `:results` | Accumulated robot results |
|
|
339
339
|
| `:messages` | Conversation history |
|
|
340
340
|
| `:session_id` | Session identifier for history persistence |
|
|
341
|
-
| `:cache` | Semantic cache
|
|
341
|
+
| `:cache` | Semantic cache (RubyLLM::SemanticCache when the optional gem is installed, else nil) |
|
|
342
342
|
|
|
343
343
|
### Reactive Memory in Networks
|
|
344
344
|
|
|
@@ -278,7 +278,10 @@ robot = RobotLab.build(
|
|
|
278
278
|
|
|
279
279
|
### Skill-Script Sandboxing (`sandbox:` section)
|
|
280
280
|
|
|
281
|
-
|
|
281
|
+
Core declares this config schema but has no sandboxing behavior of its own — enforcement
|
|
282
|
+
lives in the optional [`robot_lab-sandbox`](https://github.com/MadBomber/robot_lab-sandbox)
|
|
283
|
+
gem. Without it, skill scripts always run unconfined regardless of these values; requiring
|
|
284
|
+
it installs a `RobotLab::ScriptTool.executor` that honors them:
|
|
282
285
|
|
|
283
286
|
| Key | Default | Description |
|
|
284
287
|
|-----|---------|-------------|
|
|
@@ -35,13 +35,10 @@ RobotLab automatically installs these core dependencies:
|
|
|
35
35
|
|
|
36
36
|
| Gem | Purpose |
|
|
37
37
|
|-----|---------|
|
|
38
|
-
| `ruby_llm` (~>
|
|
38
|
+
| `ruby_llm` (~> 2.0.0.rc3) | LLM provider integrations (Anthropic, OpenAI, Gemini, etc.) |
|
|
39
39
|
| `prompt_manager` (~> 1.0) | Template-based prompt management with YAML front matter |
|
|
40
40
|
| `simple_flow` (~> 0.4) | Pipeline workflow execution for networks |
|
|
41
41
|
| `myway_config` (~> 0.1) | Layered configuration (defaults, env vars, config files) |
|
|
42
|
-
| `ruby_llm-mcp` (~> 1.0) | Model Context Protocol client for external tool servers |
|
|
43
|
-
| `ruby_llm-schema` (~> 0.3) | Schema validation for structured outputs |
|
|
44
|
-
| `ruby_llm-semantic_cache` (~> 0.1) | Semantic caching for LLM responses |
|
|
45
42
|
| `zeitwerk` (~> 2.6) | Autoloading and eager loading |
|
|
46
43
|
| `async` (~> 2.0) | Fiber-based concurrency |
|
|
47
44
|
| `async-http` (~> 0.60) | MCP SSE and streamable-HTTP transports |
|
data/docs/guides/hooks.md
CHANGED
|
@@ -1076,6 +1076,128 @@ RobotLab.on(RedactionHook)
|
|
|
1076
1076
|
|
|
1077
1077
|
---
|
|
1078
1078
|
|
|
1079
|
+
## Extension Registration
|
|
1080
|
+
|
|
1081
|
+
Hooks inject *behavior* ("do this on these events"). A separate, lighter
|
|
1082
|
+
mechanism — **extension registration** — answers a different question: *"is
|
|
1083
|
+
optional feature X loaded?"* This is how core lights up conditional behavior
|
|
1084
|
+
without depending on, or probing for, the gem that provides it. The two
|
|
1085
|
+
mechanisms are orthogonal but commonly used together: a gem registers itself
|
|
1086
|
+
(discovery), then wires behavior in via a hook (injection).
|
|
1087
|
+
|
|
1088
|
+
### The API
|
|
1089
|
+
|
|
1090
|
+
```ruby
|
|
1091
|
+
RobotLab.register_extension(:audit, RobotLab::Audit) # gem announces itself
|
|
1092
|
+
RobotLab.extension_loaded?(:audit) # => true/false — core's guard
|
|
1093
|
+
RobotLab.extension(:audit) # => RobotLab::Audit
|
|
1094
|
+
RobotLab.loaded_extensions # => [:audit, :ractor, ...]
|
|
1095
|
+
```
|
|
1096
|
+
|
|
1097
|
+
It is deliberately tiny: a `Hash` and four methods. The registered value can be
|
|
1098
|
+
the extension's primary module, or just a sentinel when the gem only needs a
|
|
1099
|
+
presence flag:
|
|
1100
|
+
|
|
1101
|
+
```ruby
|
|
1102
|
+
RobotLab.register_extension(:ractor, :ractor_extension_loaded)
|
|
1103
|
+
```
|
|
1104
|
+
|
|
1105
|
+
### How an Extension Announces Itself
|
|
1106
|
+
|
|
1107
|
+
At the bottom of the gem's entry file, after everything it provides is
|
|
1108
|
+
defined, guarded so the gem can also load standalone or against an older core:
|
|
1109
|
+
|
|
1110
|
+
```ruby
|
|
1111
|
+
# robot_lab-audit/lib/robot_lab/audit.rb (tail)
|
|
1112
|
+
if defined?(RobotLab) && RobotLab.respond_to?(:register_extension)
|
|
1113
|
+
RobotLab.register_extension(:audit, RobotLab::Audit)
|
|
1114
|
+
end
|
|
1115
|
+
```
|
|
1116
|
+
|
|
1117
|
+
The same one-liner appears verbatim in `robot_lab-document_store`,
|
|
1118
|
+
`robot_lab-durable`, `robot_lab-discovery`, etc. Registration last, guarded
|
|
1119
|
+
always.
|
|
1120
|
+
|
|
1121
|
+
### How Core Consumes It
|
|
1122
|
+
|
|
1123
|
+
Core guards optional behavior with `extension_loaded?` instead of scattered
|
|
1124
|
+
`defined?` / `respond_to?` probes — one named question, asked in one style:
|
|
1125
|
+
|
|
1126
|
+
```ruby
|
|
1127
|
+
# tool.rb — only use the Ractor pool when the gem is present
|
|
1128
|
+
self.class.ractor_safe? && !self.class.name.nil? && RobotLab.extension_loaded?(:ractor)
|
|
1129
|
+
|
|
1130
|
+
# memory.rb — semantic features require the document_store gem
|
|
1131
|
+
unless RobotLab.extension_loaded?(:document_store)
|
|
1132
|
+
…
|
|
1133
|
+
end
|
|
1134
|
+
```
|
|
1135
|
+
|
|
1136
|
+
When a guarded feature is *requested* without its extension, core raises a
|
|
1137
|
+
helpful error naming the exact gem to add — not a cryptic `NoMethodError`:
|
|
1138
|
+
|
|
1139
|
+
```ruby
|
|
1140
|
+
# network.rb — parallel_mode: :ractor needs the gem
|
|
1141
|
+
def run_with_ractor_scheduler(run_context)
|
|
1142
|
+
unless RobotLab.extension_loaded?(:ractor)
|
|
1143
|
+
raise RobotLab::DependencyError,
|
|
1144
|
+
"parallel_mode: :ractor requires the robot_lab-ractor gem. " \
|
|
1145
|
+
"Add `gem 'robot_lab-ractor'` to your Gemfile."
|
|
1146
|
+
end
|
|
1147
|
+
…
|
|
1148
|
+
end
|
|
1149
|
+
```
|
|
1150
|
+
|
|
1151
|
+
### Version-Skew Tolerance
|
|
1152
|
+
|
|
1153
|
+
`robot_lab-a2a` ships a fallback so it works even against a core too old to
|
|
1154
|
+
provide the registry API: if `register_extension` isn't defined, the gem
|
|
1155
|
+
**defines it** (plus `extension_loaded?`/`extension`) over its own private
|
|
1156
|
+
`@_extensions` hash, then registers itself:
|
|
1157
|
+
|
|
1158
|
+
```ruby
|
|
1159
|
+
module RobotLab
|
|
1160
|
+
@_extensions = {} unless instance_variable_defined?(:@_extensions)
|
|
1161
|
+
|
|
1162
|
+
class << self
|
|
1163
|
+
unless method_defined?(:register_extension) || respond_to?(:register_extension)
|
|
1164
|
+
def register_extension(name, mod) = @_extensions[name.to_sym] = mod
|
|
1165
|
+
end
|
|
1166
|
+
unless method_defined?(:extension_loaded?) || respond_to?(:extension_loaded?)
|
|
1167
|
+
def extension_loaded?(name) = @_extensions.key?(name.to_sym)
|
|
1168
|
+
end
|
|
1169
|
+
# … extension(name) likewise …
|
|
1170
|
+
end
|
|
1171
|
+
end
|
|
1172
|
+
|
|
1173
|
+
RobotLab.register_extension(:a2a, RobotLab::A2A)
|
|
1174
|
+
```
|
|
1175
|
+
|
|
1176
|
+
The gem never assumes the core's age; it provides the contract if it must.
|
|
1177
|
+
|
|
1178
|
+
### How It Composes with Hooks
|
|
1179
|
+
|
|
1180
|
+
- `register_extension(:audit, RobotLab::Audit)` — *"I exist"* (discovery).
|
|
1181
|
+
- Inside `Audit.enable(db_path:)` → `RobotLab.on(Hook)` — *"do this on these
|
|
1182
|
+
events"* (behavior).
|
|
1183
|
+
|
|
1184
|
+
Two common shapes:
|
|
1185
|
+
|
|
1186
|
+
- **Auto-wire at load** — register the extension *and* `RobotLab.on(SomeHook)`
|
|
1187
|
+
at require time (always-on features).
|
|
1188
|
+
- **Opt-in enable** — register the extension at load, but expose
|
|
1189
|
+
`enable!`/`enable(...)` that registers the hook on demand
|
|
1190
|
+
(`Audit.enable`, `Narrator.enable!`). Lets the user choose scope/config.
|
|
1191
|
+
|
|
1192
|
+
> [!TIP]
|
|
1193
|
+
> Design decisions worth stealing: a named capability registry beats
|
|
1194
|
+
> duck-typing; core never `require`s an extension — the extension `require`s
|
|
1195
|
+
> core and announces itself; a sentinel value works when you only need a
|
|
1196
|
+
> presence flag; and using a guarded feature without its gem should raise a
|
|
1197
|
+
> helpful error naming the exact gem to add.
|
|
1198
|
+
|
|
1199
|
+
---
|
|
1200
|
+
|
|
1079
1201
|
## See Also
|
|
1080
1202
|
|
|
1081
1203
|
- [examples/35_hooks.rb](https://github.com/MadBomber/robot_lab/blob/main/examples/35_hooks.rb) — full demo with xyzzy extension, perf timer, LLM response cache, and tracer hooks
|
data/docs/guides/memory.md
CHANGED
|
@@ -10,7 +10,7 @@ Memory is a reactive key-value store that provides:
|
|
|
10
10
|
- Reserved keys for structured data (`:data`, `:results`, `:messages`, `:session_id`, `:cache`)
|
|
11
11
|
- Reactive subscriptions and blocking reads for inter-robot communication
|
|
12
12
|
- Optional Redis backend for persistence
|
|
13
|
-
- Semantic caching via `RubyLLM::SemanticCache`
|
|
13
|
+
- Semantic caching via `RubyLLM::SemanticCache` (optional ruby_llm-semantic_cache gem)
|
|
14
14
|
|
|
15
15
|
## Standalone Robot Memory
|
|
16
16
|
|
|
@@ -86,7 +86,7 @@ Memory has reserved keys with special behavior:
|
|
|
86
86
|
| `:results` | Array | Accumulated robot results |
|
|
87
87
|
| `:messages` | Array | Conversation history |
|
|
88
88
|
| `:session_id` | String | Session identifier for history persistence |
|
|
89
|
-
| `:cache` | `RubyLLM::SemanticCache` module, or `nil` | Semantic cache (read-only after init). Set at construction time; `nil` when built with `enable_cache: false` |
|
|
89
|
+
| `:cache` | `RubyLLM::SemanticCache` module, or `nil` | Semantic cache (read-only after init). Set at construction time; `nil` when built with `enable_cache: false` or when the optional gem is absent |
|
|
90
90
|
|
|
91
91
|
### The Data Hash
|
|
92
92
|
|
data/docs/guides/using-tools.md
CHANGED
|
@@ -335,13 +335,22 @@ trust: external # or "core" for trusted, always-unconfined skills
|
|
|
335
335
|
---
|
|
336
336
|
```
|
|
337
337
|
|
|
338
|
-
|
|
338
|
+
`Capabilities` (the declaration above) lives in core, but core itself has **no sandboxing
|
|
339
|
+
behavior and no built-in limitations** — a script always runs unconfined via a plain
|
|
340
|
+
`Open3.capture2e`, with no timeout, unless the optional
|
|
341
|
+
[`robot_lab-sandbox`](https://github.com/MadBomber/robot_lab-sandbox) gem is required.
|
|
342
|
+
Requiring it installs `RobotLab::Sandbox::Executor` as `RobotLab::ScriptTool.executor`;
|
|
343
|
+
confinement still stays **opt-in and off by default** from there — see the
|
|
344
|
+
[`sandbox:` config section](../getting-started/configuration.md#skill-script-sandboxing-sandbox-section).
|
|
345
|
+
When enabled:
|
|
339
346
|
|
|
340
347
|
- The global `sandbox:` config is a **ceiling** (`fs_read`, `fs_write`, `network`, `timeout`); each skill's front matter is its **declared** request. The script actually runs under the **intersection** of the two — a path outside the ceiling's roots is dropped even if the skill declares it, `network` requires both sides to allow it, and `timeout` is the smaller of the two.
|
|
341
348
|
- On macOS, confinement is enforced with a generated `sandbox-exec` (Seatbelt) profile: deny-by-default, with narrow allowances for the interpreter to boot, the granted read/write paths, and (optionally) the network. Notably, `$HOME` is never implicitly readable — SSH keys and cloud credentials stay out of reach unless a path under `$HOME` is explicitly granted.
|
|
342
349
|
- Off macOS, or for any skill declaring `trust: core`, sandboxing is a passthrough — confinement is currently macOS-only and is always skipped for trusted "core" skills regardless of platform.
|
|
343
350
|
- A script that runs past its `timeout` is killed (its whole process group) and reported back to the LLM as a timed-out error rather than hanging the turn.
|
|
344
351
|
|
|
352
|
+
Full API reference: [robot_lab-sandbox](https://github.com/MadBomber/robot_lab-sandbox).
|
|
353
|
+
|
|
345
354
|
## Parameter Types
|
|
346
355
|
|
|
347
356
|
Define parameters on `RubyLLM::Tool` subclasses using `param`:
|
data/examples/.envrc
CHANGED
data/examples/02_tools.rb
CHANGED
|
@@ -14,17 +14,17 @@ require_relative "common"
|
|
|
14
14
|
class Calculator < RubyLLM::Tool
|
|
15
15
|
description "Performs basic arithmetic operations"
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
parameter :operation,
|
|
18
18
|
type: "string",
|
|
19
|
-
|
|
19
|
+
description: "The operation to perform (add, subtract, multiply, divide)"
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
parameter :a,
|
|
22
22
|
type: "number",
|
|
23
|
-
|
|
23
|
+
description: "First operand"
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
parameter :b,
|
|
26
26
|
type: "number",
|
|
27
|
-
|
|
27
|
+
description: "Second operand"
|
|
28
28
|
|
|
29
29
|
def execute(operation:, a:, b:)
|
|
30
30
|
case operation
|
|
@@ -40,9 +40,9 @@ end
|
|
|
40
40
|
class FortuneCookie < RubyLLM::Tool
|
|
41
41
|
description "Get a fortune cookie message with wisdom and lucky numbers"
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
parameter :category,
|
|
44
44
|
type: "string",
|
|
45
|
-
|
|
45
|
+
description: "The category of fortune (wisdom, love, career, adventure)"
|
|
46
46
|
|
|
47
47
|
FORTUNES = {
|
|
48
48
|
"wisdom" => [
|
data/examples/03_network.rb
CHANGED
|
@@ -40,7 +40,7 @@ end
|
|
|
40
40
|
|
|
41
41
|
# Shared RunConfig — operational defaults every robot in this network inherits.
|
|
42
42
|
#
|
|
43
|
-
# RunConfig has no `provider` field (see RunConfig::FIELDS), and an
|
|
43
|
+
# RunConfig has no `provider` field (see RunConfig::FIELDS), and an LM Studio
|
|
44
44
|
# model is absent from RubyLLM's registry, so provider and model still travel
|
|
45
45
|
# together on each robot via **llm_opts. The RunConfig carries the settings
|
|
46
46
|
# that genuinely are shared.
|
data/examples/04_mcp.rb
CHANGED
|
@@ -163,14 +163,16 @@ begin
|
|
|
163
163
|
puts "Query: 'What are the top 3 most starred Ruby web frameworks on GitHub?'"
|
|
164
164
|
hr
|
|
165
165
|
|
|
166
|
-
# mcp: :inherit
|
|
167
|
-
#
|
|
168
|
-
#
|
|
169
|
-
#
|
|
166
|
+
# mcp: :inherit is required — run() defaults it to :none, which resolves to
|
|
167
|
+
# "no MCP servers this turn" and the discovered tools would never reach the
|
|
168
|
+
# provider. tools: takes an allowlist of tool names; :inherit would send all
|
|
169
|
+
# ~90 GitHub tools, whose JSON schemas alone exceed 13k tokens and overflow
|
|
170
|
+
# a local model's context window (LM Studio's default is 8192) before the
|
|
171
|
+
# query even arrives. Send only the one tool this query needs.
|
|
170
172
|
result = robot.run(
|
|
171
173
|
"What are the top 3 most starred Ruby web frameworks on GitHub? Just list their names and star counts.",
|
|
172
174
|
mcp: :inherit,
|
|
173
|
-
tools:
|
|
175
|
+
tools: %w[search_repositories]
|
|
174
176
|
)
|
|
175
177
|
|
|
176
178
|
puts
|