robot_lab 0.2.6 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.envrc +4 -0
- data/.loki +5 -0
- data/Archspec.rb +44 -0
- data/CHANGELOG.md +17 -1
- data/README.md +124 -64
- data/Rakefile +6 -111
- data/_typos.toml +21 -0
- 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 +423 -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 +87 -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 +288 -98
- 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 +263 -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 +247 -18
- 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/agent_skill_catalog.rb +1 -0
- data/lib/robot_lab/ask_user.rb +2 -0
- data/lib/robot_lab/bus_poller.rb +2 -0
- data/lib/robot_lab/capabilities.rb +4 -0
- data/lib/robot_lab/config.rb +18 -5
- 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 +4 -0
- data/lib/robot_lab/mcp/transports/websocket.rb +3 -0
- data/lib/robot_lab/memory.rb +23 -6
- data/lib/robot_lab/memory_change.rb +1 -0
- data/lib/robot_lab/message.rb +3 -0
- data/lib/robot_lab/names.rb +400 -0
- data/lib/robot_lab/network.rb +13 -6
- data/lib/robot_lab/robot/agent_skill_matching.rb +3 -3
- data/lib/robot_lab/robot/bus_messaging.rb +19 -8
- data/lib/robot_lab/robot/history_search.rb +2 -0
- data/lib/robot_lab/robot/hooking.rb +3 -0
- data/lib/robot_lab/robot/mcp_management.rb +11 -3
- data/lib/robot_lab/robot/template_rendering.rb +29 -7
- data/lib/robot_lab/robot.rb +81 -19
- data/lib/robot_lab/robot_result.rb +3 -0
- data/lib/robot_lab/run_config.rb +5 -0
- 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 +4 -0
- 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 +27 -18
- data/mkdocs.yml +6 -1
- metadata +12 -7
- 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
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
# Skills API
|
|
2
|
+
|
|
3
|
+
Class-level reference for the AgentSkills subsystem: skill bundles, the scripts
|
|
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
|
|
10
|
+
[Using Tools: Skill Scripts and Sandboxing](../guides/using-tools.md#skill-scripts-and-sandboxing)
|
|
11
|
+
and [Building Robots: Composable Skills](../guides/building-robots.md#composable-skills).
|
|
12
|
+
|
|
13
|
+
!!! note "Two different things are called 'skills'"
|
|
14
|
+
**Template skills** (`RobotLab.build(skills: [:clarifier])`) are ordinary
|
|
15
|
+
prompt templates whose bodies are prepended to a robot's system prompt — see
|
|
16
|
+
[Robot: Skills](core/robot.md#skills). **AgentSkills** (this page) are
|
|
17
|
+
`SKILL.md` *bundles* on disk, discovered from `~/.prompts/skills/`, matched
|
|
18
|
+
to a message by embedding similarity at run time, and capable of contributing
|
|
19
|
+
executable tools. They share the word but not the mechanism.
|
|
20
|
+
|
|
21
|
+
```mermaid
|
|
22
|
+
graph LR
|
|
23
|
+
subgraph "Discovery"
|
|
24
|
+
C[AgentSkillCatalog<br/>~/.prompts/skills/]
|
|
25
|
+
S[AgentSkill<br/>SKILL.md]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
subgraph "Capability grant"
|
|
29
|
+
D[Capabilities<br/>declared in front matter]
|
|
30
|
+
CE[Capabilities.ceiling<br/>from config.sandbox]
|
|
31
|
+
G[effective grant<br/>declared ∩ ceiling]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
subgraph "Execution (core)"
|
|
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)"
|
|
41
|
+
SB{Sandbox.enabled?}
|
|
42
|
+
SE[Sandbox::Seatbelt<br/>macOS]
|
|
43
|
+
NU[Sandbox::Null<br/>passthrough]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
R[Robot<br/>AgentSkillMatching] --> C
|
|
47
|
+
C --> S
|
|
48
|
+
S --> D
|
|
49
|
+
S --> T
|
|
50
|
+
D --> G
|
|
51
|
+
CE --> G
|
|
52
|
+
T --> EX
|
|
53
|
+
EX -- "nil (default)" --> UN
|
|
54
|
+
EX -- "installed" --> SB
|
|
55
|
+
SB -- "off, or trust: core,<br/>or non-macOS" --> NU
|
|
56
|
+
SB -- "on + macOS" --> SE
|
|
57
|
+
G --> SE
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## RobotLab::AgentSkill
|
|
63
|
+
|
|
64
|
+
Immutable value object for one skill folder: a directory containing a `SKILL.md`
|
|
65
|
+
with `name` and `description` front matter, plus optional `scripts/`,
|
|
66
|
+
`references/`, and `assets/` subdirectories.
|
|
67
|
+
|
|
68
|
+
### Constructor
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
skill = RobotLab::AgentSkill.new("~/.prompts/skills/deploy-checker/SKILL.md")
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
| Name | Type | Description |
|
|
75
|
+
|------|------|-------------|
|
|
76
|
+
| `skill_md_path` | `String`, `Pathname` | Path to the `SKILL.md` file itself, **not** the directory |
|
|
77
|
+
|
|
78
|
+
**Raises `RobotLab::ConfigurationError`** when front matter is missing `name` or
|
|
79
|
+
`description` (or either is blank) — and because a file with no `---` block
|
|
80
|
+
parses to an empty hash, a `SKILL.md` without front matter always raises.
|
|
81
|
+
Malformed YAML raises `Psych::SyntaxError` instead, straight from
|
|
82
|
+
`YAML.safe_load`. [`AgentSkillCatalog`](#robotlabagentskillcatalog) rescues both
|
|
83
|
+
and skips the bundle; construct an `AgentSkill` directly and you get the
|
|
84
|
+
exception.
|
|
85
|
+
|
|
86
|
+
### Attributes
|
|
87
|
+
|
|
88
|
+
| Attribute | Type | Description |
|
|
89
|
+
|-----------|------|-------------|
|
|
90
|
+
| `name` | `String` | Front-matter `name`; also the catalog lookup key (symbolized) |
|
|
91
|
+
| `description` | `String` | Front-matter `description`; the text matched against the user's message |
|
|
92
|
+
| `path` | `Pathname` | The skill **directory** (`dirname` of the `SKILL.md` path) |
|
|
93
|
+
| `capabilities` | `Capabilities` | Built from front matter via `Capabilities.from_front_matter` |
|
|
94
|
+
|
|
95
|
+
### instructions
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
skill.instructions # => String
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The `SKILL.md` body below the front matter, stripped. This is the text
|
|
102
|
+
`Robot::AgentSkillMatching` prepends to the system prompt when the skill matches.
|
|
103
|
+
Memoized.
|
|
104
|
+
|
|
105
|
+
### scripts
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
skill.scripts # => Array<Pathname>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Every **file** directly inside the skill's `scripts/` directory, sorted. Returns
|
|
112
|
+
`[]` when there is no `scripts/` directory. Not recursive — subdirectories are
|
|
113
|
+
skipped. Memoized.
|
|
114
|
+
|
|
115
|
+
### script_tools
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
skill.script_tools # => Array<RobotLab::Tool>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
One `RobotLab::Tool` per script, built with
|
|
122
|
+
[`ScriptTool.from_path`](#scripttoolfrom_path) and carrying this skill's
|
|
123
|
+
`capabilities` and directory. **Non-executable scripts are skipped** (logged at
|
|
124
|
+
`warn` and filtered out by `filter_map`), so this array can be shorter than
|
|
125
|
+
`scripts`. Memoized.
|
|
126
|
+
|
|
127
|
+
These tools are appended to `robot.local_tools` for the duration of a matched
|
|
128
|
+
`run` and removed again in the `ensure` block — see
|
|
129
|
+
[Robot Execution](../architecture/robot-execution.md#execution-overview).
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## RobotLab::AgentSkillCatalog
|
|
134
|
+
|
|
135
|
+
Lazily-loaded registry of the skill folders under a root directory.
|
|
136
|
+
|
|
137
|
+
### SKILLS_ROOT
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
RobotLab::AgentSkillCatalog::SKILLS_ROOT
|
|
141
|
+
# => #<Pathname:/Users/you/.prompts/skills>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`~/.prompts/skills`, expanded at load time. The path the process-level singleton
|
|
145
|
+
scans.
|
|
146
|
+
|
|
147
|
+
### instance / reset!
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
RobotLab::AgentSkillCatalog.instance # => the singleton, scanning SKILLS_ROOT
|
|
151
|
+
RobotLab::AgentSkillCatalog.reset! # => nil; next `instance` builds a fresh one
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`instance` memoizes. `reset!` drops the memo — it exists so tests can point the
|
|
155
|
+
catalog at a fixture directory by resetting and constructing an instance
|
|
156
|
+
explicitly with a different root.
|
|
157
|
+
|
|
158
|
+
### Constructor
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
catalog = RobotLab::AgentSkillCatalog.new("/path/to/skills")
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
| Name | Type | Default | Description |
|
|
165
|
+
|------|------|---------|-------------|
|
|
166
|
+
| `skills_root` | `String`, `Pathname` | `SKILLS_ROOT` | Directory to scan |
|
|
167
|
+
|
|
168
|
+
Construction does **no** I/O; the scan happens on the first `find`/`all`.
|
|
169
|
+
|
|
170
|
+
### find
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
catalog.find(:deploy_checker) # => AgentSkill or nil
|
|
174
|
+
catalog.find("deploy-checker") # => AgentSkill or nil
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Look up by skill **name** (the front-matter `name`, symbolized) — not by
|
|
178
|
+
directory name, and not by file path. Returns `nil` when not found.
|
|
179
|
+
|
|
180
|
+
### all
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
catalog.all # => Array<AgentSkill>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Every successfully-loaded skill.
|
|
187
|
+
|
|
188
|
+
!!! note "Loading is lazy, thread-safe, and forgiving"
|
|
189
|
+
The scan runs once, under a `Mutex`, on the first `find` or `all`. A missing
|
|
190
|
+
root directory is not an error — the catalog is simply empty. A directory
|
|
191
|
+
without a `SKILL.md` is skipped silently; a `SKILL.md` that raises
|
|
192
|
+
`ConfigurationError` or `Psych::SyntaxError` is skipped with a `warn`
|
|
193
|
+
(`"AgentSkillCatalog: <message>, skipping <dir>"`). One bad bundle never
|
|
194
|
+
prevents the others from loading, and the scan is never retried.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## RobotLab::Capabilities
|
|
199
|
+
|
|
200
|
+
What a skill's scripts may read, write, reach, and how long they may run.
|
|
201
|
+
|
|
202
|
+
A skill declares what it **wants** in `SKILL.md` front matter; the global
|
|
203
|
+
`sandbox:` config declares the **ceiling**. The effective grant is the
|
|
204
|
+
[intersection](#intersect) of the two.
|
|
205
|
+
|
|
206
|
+
### Constants
|
|
207
|
+
|
|
208
|
+
| Constant | Value | Description |
|
|
209
|
+
|----------|-------|-------------|
|
|
210
|
+
| `DEFAULT_TIMEOUT` | `60` | Seconds, used when `timeout` is absent or non-positive |
|
|
211
|
+
| `TRUST_LEVELS` | `["core", "external"]` | Any other value falls back to `"external"` |
|
|
212
|
+
|
|
213
|
+
### Constructor
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
RobotLab::Capabilities.new(
|
|
217
|
+
fs_read: [], fs_write: [], network: false,
|
|
218
|
+
timeout: DEFAULT_TIMEOUT, trust: "external"
|
|
219
|
+
)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
| Name | Type | Default | Coercion |
|
|
223
|
+
|------|------|---------|----------|
|
|
224
|
+
| `fs_read` | `Array<String>` | `[]` | `Array(...)` then `to_s` on each entry |
|
|
225
|
+
| `fs_write` | `Array<String>` | `[]` | Same |
|
|
226
|
+
| `network` | `Boolean` | `false` | Any truthy value becomes `true` |
|
|
227
|
+
| `timeout` | `Integer` | `60` | `to_i`; anything not positive becomes `DEFAULT_TIMEOUT` |
|
|
228
|
+
| `trust` | `String` | `"external"` | Must be in `TRUST_LEVELS`, else `"external"` |
|
|
229
|
+
|
|
230
|
+
Every value is normalized in the constructor, so the readers `fs_read`,
|
|
231
|
+
`fs_write`, `network`, `timeout`, and `trust` always return well-formed values —
|
|
232
|
+
a malformed `SKILL.md` degrades to the safe default rather than raising.
|
|
233
|
+
|
|
234
|
+
### from_front_matter
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
RobotLab::Capabilities.from_front_matter(front_matter_hash) # => Capabilities
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Build from a parsed `SKILL.md` front-matter hash, reading `fs_read`, `fs_write`,
|
|
241
|
+
`network`, `timeout`, and `trust`. A `nil` front matter yields an all-defaults
|
|
242
|
+
instance.
|
|
243
|
+
|
|
244
|
+
### fm_value
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
RobotLab::Capabilities.fm_value(front_matter, :network, false)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Look up a front-matter key tolerating **either** string or symbol keys (string
|
|
251
|
+
first, then symbol), returning `default` when both are `nil`. Exposed because
|
|
252
|
+
`from_front_matter` uses it and skill-tooling may need the same leniency.
|
|
253
|
+
|
|
254
|
+
### ceiling
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
RobotLab::Capabilities.ceiling # => from RobotLab.config.sandbox
|
|
258
|
+
RobotLab::Capabilities.ceiling(custom_config)
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
The maximum grant any skill may receive, read from the config's `sandbox:`
|
|
262
|
+
section. When there is no `sandbox` section at all, the ceiling is
|
|
263
|
+
`Capabilities.new(fs_read: ["."])` — read-only access to the working directory,
|
|
264
|
+
no writes, no network.
|
|
265
|
+
|
|
266
|
+
Note the ceiling never carries a `trust` — trust is a property of the skill, not
|
|
267
|
+
of the ceiling, and `intersect` keeps the declared value.
|
|
268
|
+
|
|
269
|
+
### core?
|
|
270
|
+
|
|
271
|
+
```ruby
|
|
272
|
+
capabilities.core? # => trust == "core"
|
|
273
|
+
```
|
|
274
|
+
|
|
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.
|
|
278
|
+
|
|
279
|
+
### intersect
|
|
280
|
+
|
|
281
|
+
```ruby
|
|
282
|
+
grant = declared.intersect(RobotLab::Capabilities.ceiling)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
The effective grant. Per field:
|
|
286
|
+
|
|
287
|
+
| Field | Rule |
|
|
288
|
+
|-------|------|
|
|
289
|
+
| `fs_read` / `fs_write` | A requested path survives only when it **is** a ceiling root or lives beneath one. Both sides are `File.expand_path`ed before comparison, so `~` and relative paths resolve first |
|
|
290
|
+
| `network` | `declared && ceiling` — both must allow it |
|
|
291
|
+
| `timeout` | The **smaller** of the two |
|
|
292
|
+
| `trust` | The **declared** value, unchanged |
|
|
293
|
+
|
|
294
|
+
Because the check is prefix-based on expanded paths, a ceiling of `["."]` grants
|
|
295
|
+
nothing outside the working directory even if a skill asks for `/etc`.
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## RobotLab::ScriptTool
|
|
300
|
+
|
|
301
|
+
Factory module that turns an executable script into a `RobotLab::Tool`. All
|
|
302
|
+
methods are module functions.
|
|
303
|
+
|
|
304
|
+
### ScriptTool.from_path
|
|
305
|
+
|
|
306
|
+
```ruby
|
|
307
|
+
tool = RobotLab::ScriptTool.from_path(script_path, capabilities: nil, skill_dir: nil)
|
|
308
|
+
# => RobotLab::Tool, or nil
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
| Name | Type | Default | Description |
|
|
312
|
+
|------|------|---------|-------------|
|
|
313
|
+
| `script_path` | `String`, `Pathname` | **required** | The script file |
|
|
314
|
+
| `capabilities` | `Capabilities`, `nil` | `nil` → `Capabilities.new` | The skill's declared capabilities |
|
|
315
|
+
| `skill_dir` | `String`, `nil` | `nil` → the script's own directory | Bundle root; always granted read access under Seatbelt |
|
|
316
|
+
|
|
317
|
+
**Returns `nil`** when the file is not executable, logging
|
|
318
|
+
`"ScriptTool: <basename> is not executable, skipping"` at `warn`. It never raises.
|
|
319
|
+
|
|
320
|
+
The generated tool takes a single optional `args` string parameter, which is
|
|
321
|
+
`Shellwords.split` and appended to `bash <script>`. Its name comes from
|
|
322
|
+
[`derive_name`](#scripttoolderive_name) and its description from
|
|
323
|
+
[`extract_description`](#scripttoolextract_description).
|
|
324
|
+
|
|
325
|
+
### ScriptTool.executor
|
|
326
|
+
|
|
327
|
+
```ruby
|
|
328
|
+
RobotLab::ScriptTool.executor # => #call, or nil (the default)
|
|
329
|
+
RobotLab::ScriptTool.executor = obj # any object responding to
|
|
330
|
+
# call(cmd, capabilities:, skill_dir:)
|
|
331
|
+
```
|
|
332
|
+
|
|
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.
|
|
337
|
+
|
|
338
|
+
### ScriptTool.execute
|
|
339
|
+
|
|
340
|
+
```ruby
|
|
341
|
+
RobotLab::ScriptTool.execute(cmd, capabilities:, skill_dir:) # => String
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Run a command array and return its combined stdout+stderr, or an error string.
|
|
345
|
+
Two paths:
|
|
346
|
+
|
|
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.
|
|
351
|
+
|
|
352
|
+
### ScriptTool.format_result
|
|
353
|
+
|
|
354
|
+
```ruby
|
|
355
|
+
RobotLab::ScriptTool.format_result(output, status) # => String
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
| `status` | Result |
|
|
359
|
+
|----------|--------|
|
|
360
|
+
| `nil` | `"Error (timed out):\n<output>"` |
|
|
361
|
+
| success | `output` verbatim |
|
|
362
|
+
| non-zero exit | `"Error (exit <N>):\n<output>"` |
|
|
363
|
+
|
|
364
|
+
Failures come back as **text for the LLM**, not exceptions — the model sees the
|
|
365
|
+
error and can adapt.
|
|
366
|
+
|
|
367
|
+
### ScriptTool.derive_name
|
|
368
|
+
|
|
369
|
+
```ruby
|
|
370
|
+
RobotLab::ScriptTool.derive_name(Pathname.new("check-deploy.sh")) # => "check_deploy"
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Strips the final extension, replaces every run of non-alphanumerics with `_`, and
|
|
374
|
+
trims leading/trailing underscores.
|
|
375
|
+
|
|
376
|
+
### ScriptTool.extract_description
|
|
377
|
+
|
|
378
|
+
```ruby
|
|
379
|
+
RobotLab::ScriptTool.extract_description(path) # => String
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
The first non-shebang comment line in the file, with leading `#` and whitespace
|
|
383
|
+
removed. Falls back to `derive_name(path)` when there is no comment or the file
|
|
384
|
+
cannot be read.
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
#!/usr/bin/env bash
|
|
388
|
+
# Verifies a deployment's health before promoting it. <- becomes the description
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## Confinement: robot_lab-sandbox
|
|
394
|
+
|
|
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):
|
|
400
|
+
|
|
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).
|
|
412
|
+
|
|
413
|
+
Full reference lives in that gem's own docs.
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## See Also
|
|
418
|
+
|
|
419
|
+
- [Using Tools: Skill Scripts and Sandboxing](../guides/using-tools.md#skill-scripts-and-sandboxing)
|
|
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
|
|
422
|
+
- [Robot: Skills](core/robot.md#skills) — template skills, the other meaning
|
|
423
|
+
- [Tool](core/tool.md) — `Tool.create`, which `ScriptTool` builds on
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Manages streaming event publishing with automatic sequencing, timestamping, and ID generation.
|
|
4
4
|
|
|
5
|
+
> **Not wired into the framework.** No robot, network, task, or hook ever
|
|
6
|
+
> constructs a `Streaming::Context` — `grep -rn "Streaming::" lib/` finds nothing
|
|
7
|
+
> outside `lib/robot_lab/streaming/`. The class works, but it only publishes what
|
|
8
|
+
> *you* hand it. For token streaming from a robot, use `on_content:` or the block
|
|
9
|
+
> form of `run`; see the [Streaming overview](index.md#the-real-streaming-api).
|
|
10
|
+
|
|
5
11
|
## Class: `RobotLab::Streaming::Context`
|
|
6
12
|
|
|
7
13
|
```ruby
|
|
@@ -157,10 +163,20 @@ Create a new context that shares the same sequence counter as this context, but
|
|
|
157
163
|
### generate_part_id
|
|
158
164
|
|
|
159
165
|
```ruby
|
|
160
|
-
context.generate_part_id # => "
|
|
166
|
+
context.generate_part_id # => "part_37ac1d49_815007_8f522876"
|
|
161
167
|
```
|
|
162
168
|
|
|
163
|
-
Generate an OpenAI-compatible part ID (max 40 characters)
|
|
169
|
+
Generate an OpenAI-compatible part ID (max 40 characters), assembled as
|
|
170
|
+
`"part_<msg>_<ts>_<rand>"`:
|
|
171
|
+
|
|
172
|
+
| Segment | Source | Length |
|
|
173
|
+
|---------|--------|--------|
|
|
174
|
+
| `<msg>` | First 8 characters of **`message_id`** (not `run_id`) | 8 |
|
|
175
|
+
| `<ts>` | Last 6 digits of the millisecond Unix timestamp | 6 |
|
|
176
|
+
| `<rand>` | `SecureRandom.hex(4)` | 8 hex chars |
|
|
177
|
+
|
|
178
|
+
Note this reads `message_id`, so two contexts sharing a message produce IDs with
|
|
179
|
+
the same first segment.
|
|
164
180
|
|
|
165
181
|
### generate_step_id
|
|
166
182
|
|
|
@@ -168,7 +184,11 @@ Generate an OpenAI-compatible part ID (max 40 characters). Combines a truncated
|
|
|
168
184
|
context.generate_step_id("text_output") # => "publish-3:text_output"
|
|
169
185
|
```
|
|
170
186
|
|
|
171
|
-
Generate a step ID for durable execution compatibility
|
|
187
|
+
Generate a step ID for durable execution compatibility, formatted as
|
|
188
|
+
`"publish-<n>:<base_name>"`. It reads the counter with `current`, which does
|
|
189
|
+
**not** increment: `<n>` is the sequence number of the most recently published
|
|
190
|
+
event (`0` before anything has been published), so calling it repeatedly between
|
|
191
|
+
publishes yields the same ID.
|
|
172
192
|
|
|
173
193
|
**Parameters:**
|
|
174
194
|
|
|
@@ -206,10 +226,13 @@ context.publish_event(event: "text.delta", data: { delta: "world!" })
|
|
|
206
226
|
context.publish_event(event: "run.completed", data: {})
|
|
207
227
|
```
|
|
208
228
|
|
|
209
|
-
###
|
|
229
|
+
### Modelling a Multi-Robot Run with Child Contexts
|
|
230
|
+
|
|
231
|
+
`RobotLab::Network` does not do any of this for you — the nesting below is
|
|
232
|
+
something you would write by hand around your own orchestration.
|
|
210
233
|
|
|
211
234
|
```ruby
|
|
212
|
-
#
|
|
235
|
+
# Parent context
|
|
213
236
|
network_ctx = RobotLab::Streaming::Context.new(
|
|
214
237
|
run_id: "net_run_1",
|
|
215
238
|
message_id: "net_msg_1",
|
data/docs/api/streaming/index.md
CHANGED
|
@@ -1,10 +1,50 @@
|
|
|
1
1
|
# Streaming
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A standalone event-publishing toolkit. **Not** the live streaming path.
|
|
4
|
+
|
|
5
|
+
## Status: not wired in
|
|
6
|
+
|
|
7
|
+
`RobotLab::Streaming::Context`, `Streaming::Events`, and
|
|
8
|
+
`Streaming::SequenceCounter` exist and work, but **nothing in the framework uses
|
|
9
|
+
them**. `grep -rn "Streaming::" lib/` returns zero hits outside
|
|
10
|
+
`lib/robot_lab/streaming/` itself: no robot, network, task, or hook ever
|
|
11
|
+
constructs a `Streaming::Context`, and no framework code publishes any of the
|
|
12
|
+
events listed below. Enabling this module does not make a robot stream.
|
|
13
|
+
|
|
14
|
+
Treat this as a set of building blocks you may drive yourself — a vocabulary of
|
|
15
|
+
event names plus a sequencing/ID helper — if you are writing your own
|
|
16
|
+
event-broadcast layer (a websocket relay, a SSE endpoint, an audit feed).
|
|
17
|
+
|
|
18
|
+
### The real streaming API
|
|
19
|
+
|
|
20
|
+
To actually receive tokens as a robot generates them, use `on_content:` and/or a
|
|
21
|
+
block on `run`:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
# Constructor / RunConfig callback — fires on every run
|
|
25
|
+
robot = RobotLab.build(
|
|
26
|
+
name: "assistant",
|
|
27
|
+
system_prompt: "You are helpful.",
|
|
28
|
+
on_content: ->(chunk) { print chunk.content }
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Or a block passed to run
|
|
32
|
+
robot.run("Tell me a story") { |chunk| print chunk.content }
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Each callback receives a `RubyLLM::Chunk`. Use `chunk.content` — there is no
|
|
36
|
+
`chunk.text`. If both are supplied, both fire, with the stored `on_content`
|
|
37
|
+
first. `on_content` is read from the robot's own config at construction time; a
|
|
38
|
+
network-level `config:` does not supply it.
|
|
39
|
+
|
|
40
|
+
The config key `streaming_enabled` has zero consumers in `lib/` and does nothing.
|
|
4
41
|
|
|
5
42
|
## Overview
|
|
6
43
|
|
|
7
|
-
The
|
|
44
|
+
The module provides structured event publishing with automatic sequencing,
|
|
45
|
+
timestamping, and ID generation. Event names cover run lifecycle, content deltas
|
|
46
|
+
(token streaming), tool calls, and metadata updates, and contexts can be nested
|
|
47
|
+
so a network-level run and its child robot runs share one monotonic sequence.
|
|
8
48
|
|
|
9
49
|
```ruby
|
|
10
50
|
publish = ->(event) {
|
|
@@ -32,25 +72,26 @@ context.publish_event(event: "text.delta", data: { delta: "Hello" })
|
|
|
32
72
|
|-----------|-------------|
|
|
33
73
|
| [Context](context.md) | Manages streaming state, sequencing, and event publishing |
|
|
34
74
|
| [Events](events.md) | Event type constants and classification helpers |
|
|
35
|
-
|
|
36
|
-
Also used internally:
|
|
37
|
-
|
|
38
|
-
| Component | Description |
|
|
39
|
-
|-----------|-------------|
|
|
40
|
-
| `SequenceCounter` | Thread-safe monotonic counter for event ordering |
|
|
75
|
+
| `SequenceCounter` | Thread-safe monotonic counter for event ordering. Its only consumer is `Streaming::Context`, which is itself unused by the framework |
|
|
41
76
|
|
|
42
77
|
## Event Categories
|
|
43
78
|
|
|
79
|
+
These are the names defined in `Streaming::Events`. They are constants and
|
|
80
|
+
classification helpers only — no framework code emits any of them.
|
|
81
|
+
|
|
44
82
|
| Category | Events | Description |
|
|
45
83
|
|----------|--------|-------------|
|
|
46
84
|
| Lifecycle | `run.started`, `run.completed`, `run.failed`, `run.interrupted` | Run-level state changes |
|
|
47
85
|
| Steps | `step.started`, `step.completed`, `step.failed` | Durable execution steps |
|
|
48
86
|
| Parts | `part.created`, `part.completed`, `part.failed` | Message composition parts |
|
|
49
|
-
| Deltas | `text.delta`, `tool_call.arguments.delta`, `reasoning.delta`, `data.delta` | Token-level content streaming |
|
|
87
|
+
| Deltas | `text.delta`, `tool_call.arguments.delta`, `tool_call.output.delta`, `reasoning.delta`, `data.delta` | Token-level content streaming |
|
|
50
88
|
| HITL | `hitl.requested`, `hitl.resolved` | Human-in-the-loop events |
|
|
51
89
|
| Metadata | `usage.updated`, `metadata.updated` | Token usage and metadata |
|
|
52
90
|
| Terminal | `stream.ended` | End of stream signal |
|
|
53
91
|
|
|
92
|
+
`DELTA_EVENTS` has five members — `tool_call.output.delta` is easy to miss.
|
|
93
|
+
`ALL_EVENTS` has twenty.
|
|
94
|
+
|
|
54
95
|
## Event Structure
|
|
55
96
|
|
|
56
97
|
Each published event is a hash with the following shape:
|
|
@@ -88,10 +129,14 @@ context.publish_event(event: "text.delta", data: { delta: "world!" })
|
|
|
88
129
|
context.publish_event(event: "run.completed", data: {})
|
|
89
130
|
```
|
|
90
131
|
|
|
91
|
-
### Nested Contexts
|
|
132
|
+
### Nested Contexts
|
|
133
|
+
|
|
134
|
+
Contexts can be nested to model a parent run with child runs sharing one
|
|
135
|
+
sequence. Note that no `Network` creates these — you would build the hierarchy
|
|
136
|
+
yourself.
|
|
92
137
|
|
|
93
138
|
```ruby
|
|
94
|
-
# Parent context for
|
|
139
|
+
# Parent context for a run you are orchestrating
|
|
95
140
|
network_context = RobotLab::Streaming::Context.new(
|
|
96
141
|
run_id: "network_run_1",
|
|
97
142
|
message_id: "msg_1",
|
|
@@ -108,3 +153,4 @@ robot_context.publish_event(event: "text.delta", data: { delta: "Response" })
|
|
|
108
153
|
|
|
109
154
|
- [Context](context.md)
|
|
110
155
|
- [Events](events.md)
|
|
156
|
+
- [Robot](../core/robot.md) -- `on_content:`, and the block form of `run`, which is how streaming actually works
|