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
data/docs/api/skills.md
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
# Skills API
|
|
2
|
+
|
|
3
|
+
Class-level reference for the AgentSkills subsystem: skill bundles, the scripts
|
|
4
|
+
they expose as tools, the capabilities those scripts declare, and the sandbox
|
|
5
|
+
that confines them. For the how-to, see
|
|
6
|
+
[Using Tools: Skill Scripts and Sandboxing](../guides/using-tools.md#skill-scripts-and-sandboxing)
|
|
7
|
+
and [Building Robots: Composable Skills](../guides/building-robots.md#composable-skills).
|
|
8
|
+
|
|
9
|
+
!!! note "Two different things are called 'skills'"
|
|
10
|
+
**Template skills** (`RobotLab.build(skills: [:clarifier])`) are ordinary
|
|
11
|
+
prompt templates whose bodies are prepended to a robot's system prompt — see
|
|
12
|
+
[Robot: Skills](core/robot.md#skills). **AgentSkills** (this page) are
|
|
13
|
+
`SKILL.md` *bundles* on disk, discovered from `~/.prompts/skills/`, matched
|
|
14
|
+
to a message by embedding similarity at run time, and capable of contributing
|
|
15
|
+
executable tools. They share the word but not the mechanism.
|
|
16
|
+
|
|
17
|
+
```mermaid
|
|
18
|
+
graph LR
|
|
19
|
+
subgraph "Discovery"
|
|
20
|
+
C[AgentSkillCatalog<br/>~/.prompts/skills/]
|
|
21
|
+
S[AgentSkill<br/>SKILL.md]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
subgraph "Capability grant"
|
|
25
|
+
D[Capabilities<br/>declared in front matter]
|
|
26
|
+
CE[Capabilities.ceiling<br/>from config.sandbox]
|
|
27
|
+
G[effective grant<br/>declared ∩ ceiling]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
subgraph "Execution"
|
|
31
|
+
T[ScriptTool.from_path<br/>-> RobotLab::Tool]
|
|
32
|
+
SB{Sandbox.enabled?}
|
|
33
|
+
SE[Sandbox::Seatbelt<br/>macOS]
|
|
34
|
+
NU[Sandbox::Null<br/>passthrough]
|
|
35
|
+
P[script process]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
R[Robot<br/>AgentSkillMatching] --> C
|
|
39
|
+
C --> S
|
|
40
|
+
S --> D
|
|
41
|
+
S --> T
|
|
42
|
+
D --> G
|
|
43
|
+
CE --> G
|
|
44
|
+
T --> SB
|
|
45
|
+
SB -- "off, or trust: core,<br/>or non-macOS" --> NU
|
|
46
|
+
SB -- "on + macOS" --> SE
|
|
47
|
+
G --> SE
|
|
48
|
+
SE --> P
|
|
49
|
+
NU --> P
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## RobotLab::AgentSkill
|
|
55
|
+
|
|
56
|
+
Immutable value object for one skill folder: a directory containing a `SKILL.md`
|
|
57
|
+
with `name` and `description` front matter, plus optional `scripts/`,
|
|
58
|
+
`references/`, and `assets/` subdirectories.
|
|
59
|
+
|
|
60
|
+
### Constructor
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
skill = RobotLab::AgentSkill.new("~/.prompts/skills/deploy-checker/SKILL.md")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
| Name | Type | Description |
|
|
67
|
+
|------|------|-------------|
|
|
68
|
+
| `skill_md_path` | `String`, `Pathname` | Path to the `SKILL.md` file itself, **not** the directory |
|
|
69
|
+
|
|
70
|
+
**Raises `RobotLab::ConfigurationError`** when front matter is missing `name` or
|
|
71
|
+
`description` (or either is blank) — and because a file with no `---` block
|
|
72
|
+
parses to an empty hash, a `SKILL.md` without front matter always raises.
|
|
73
|
+
Malformed YAML raises `Psych::SyntaxError` instead, straight from
|
|
74
|
+
`YAML.safe_load`. [`AgentSkillCatalog`](#robotlabagentskillcatalog) rescues both
|
|
75
|
+
and skips the bundle; construct an `AgentSkill` directly and you get the
|
|
76
|
+
exception.
|
|
77
|
+
|
|
78
|
+
### Attributes
|
|
79
|
+
|
|
80
|
+
| Attribute | Type | Description |
|
|
81
|
+
|-----------|------|-------------|
|
|
82
|
+
| `name` | `String` | Front-matter `name`; also the catalog lookup key (symbolized) |
|
|
83
|
+
| `description` | `String` | Front-matter `description`; the text matched against the user's message |
|
|
84
|
+
| `path` | `Pathname` | The skill **directory** (`dirname` of the `SKILL.md` path) |
|
|
85
|
+
| `capabilities` | `Capabilities` | Built from front matter via `Capabilities.from_front_matter` |
|
|
86
|
+
|
|
87
|
+
### instructions
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
skill.instructions # => String
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The `SKILL.md` body below the front matter, stripped. This is the text
|
|
94
|
+
`Robot::AgentSkillMatching` prepends to the system prompt when the skill matches.
|
|
95
|
+
Memoized.
|
|
96
|
+
|
|
97
|
+
### scripts
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
skill.scripts # => Array<Pathname>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Every **file** directly inside the skill's `scripts/` directory, sorted. Returns
|
|
104
|
+
`[]` when there is no `scripts/` directory. Not recursive — subdirectories are
|
|
105
|
+
skipped. Memoized.
|
|
106
|
+
|
|
107
|
+
### script_tools
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
skill.script_tools # => Array<RobotLab::Tool>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
One `RobotLab::Tool` per script, built with
|
|
114
|
+
[`ScriptTool.from_path`](#scripttoolfrom_path) and carrying this skill's
|
|
115
|
+
`capabilities` and directory. **Non-executable scripts are skipped** (logged at
|
|
116
|
+
`warn` and filtered out by `filter_map`), so this array can be shorter than
|
|
117
|
+
`scripts`. Memoized.
|
|
118
|
+
|
|
119
|
+
These tools are appended to `robot.local_tools` for the duration of a matched
|
|
120
|
+
`run` and removed again in the `ensure` block — see
|
|
121
|
+
[Robot Execution](../architecture/robot-execution.md#execution-overview).
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## RobotLab::AgentSkillCatalog
|
|
126
|
+
|
|
127
|
+
Lazily-loaded registry of the skill folders under a root directory.
|
|
128
|
+
|
|
129
|
+
### SKILLS_ROOT
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
RobotLab::AgentSkillCatalog::SKILLS_ROOT
|
|
133
|
+
# => #<Pathname:/Users/you/.prompts/skills>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`~/.prompts/skills`, expanded at load time. The path the process-level singleton
|
|
137
|
+
scans.
|
|
138
|
+
|
|
139
|
+
### instance / reset!
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
RobotLab::AgentSkillCatalog.instance # => the singleton, scanning SKILLS_ROOT
|
|
143
|
+
RobotLab::AgentSkillCatalog.reset! # => nil; next `instance` builds a fresh one
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`instance` memoizes. `reset!` drops the memo — it exists so tests can point the
|
|
147
|
+
catalog at a fixture directory by resetting and constructing an instance
|
|
148
|
+
explicitly with a different root.
|
|
149
|
+
|
|
150
|
+
### Constructor
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
catalog = RobotLab::AgentSkillCatalog.new("/path/to/skills")
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
| Name | Type | Default | Description |
|
|
157
|
+
|------|------|---------|-------------|
|
|
158
|
+
| `skills_root` | `String`, `Pathname` | `SKILLS_ROOT` | Directory to scan |
|
|
159
|
+
|
|
160
|
+
Construction does **no** I/O; the scan happens on the first `find`/`all`.
|
|
161
|
+
|
|
162
|
+
### find
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
catalog.find(:deploy_checker) # => AgentSkill or nil
|
|
166
|
+
catalog.find("deploy-checker") # => AgentSkill or nil
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Look up by skill **name** (the front-matter `name`, symbolized) — not by
|
|
170
|
+
directory name, and not by file path. Returns `nil` when not found.
|
|
171
|
+
|
|
172
|
+
### all
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
catalog.all # => Array<AgentSkill>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Every successfully-loaded skill.
|
|
179
|
+
|
|
180
|
+
!!! note "Loading is lazy, thread-safe, and forgiving"
|
|
181
|
+
The scan runs once, under a `Mutex`, on the first `find` or `all`. A missing
|
|
182
|
+
root directory is not an error — the catalog is simply empty. A directory
|
|
183
|
+
without a `SKILL.md` is skipped silently; a `SKILL.md` that raises
|
|
184
|
+
`ConfigurationError` or `Psych::SyntaxError` is skipped with a `warn`
|
|
185
|
+
(`"AgentSkillCatalog: <message>, skipping <dir>"`). One bad bundle never
|
|
186
|
+
prevents the others from loading, and the scan is never retried.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## RobotLab::Capabilities
|
|
191
|
+
|
|
192
|
+
What a skill's scripts may read, write, reach, and how long they may run.
|
|
193
|
+
|
|
194
|
+
A skill declares what it **wants** in `SKILL.md` front matter; the global
|
|
195
|
+
`sandbox:` config declares the **ceiling**. The effective grant is the
|
|
196
|
+
[intersection](#intersect) of the two.
|
|
197
|
+
|
|
198
|
+
### Constants
|
|
199
|
+
|
|
200
|
+
| Constant | Value | Description |
|
|
201
|
+
|----------|-------|-------------|
|
|
202
|
+
| `DEFAULT_TIMEOUT` | `60` | Seconds, used when `timeout` is absent or non-positive |
|
|
203
|
+
| `TRUST_LEVELS` | `["core", "external"]` | Any other value falls back to `"external"` |
|
|
204
|
+
|
|
205
|
+
### Constructor
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
RobotLab::Capabilities.new(
|
|
209
|
+
fs_read: [], fs_write: [], network: false,
|
|
210
|
+
timeout: DEFAULT_TIMEOUT, trust: "external"
|
|
211
|
+
)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
| Name | Type | Default | Coercion |
|
|
215
|
+
|------|------|---------|----------|
|
|
216
|
+
| `fs_read` | `Array<String>` | `[]` | `Array(...)` then `to_s` on each entry |
|
|
217
|
+
| `fs_write` | `Array<String>` | `[]` | Same |
|
|
218
|
+
| `network` | `Boolean` | `false` | Any truthy value becomes `true` |
|
|
219
|
+
| `timeout` | `Integer` | `60` | `to_i`; anything not positive becomes `DEFAULT_TIMEOUT` |
|
|
220
|
+
| `trust` | `String` | `"external"` | Must be in `TRUST_LEVELS`, else `"external"` |
|
|
221
|
+
|
|
222
|
+
Every value is normalized in the constructor, so the readers `fs_read`,
|
|
223
|
+
`fs_write`, `network`, `timeout`, and `trust` always return well-formed values —
|
|
224
|
+
a malformed `SKILL.md` degrades to the safe default rather than raising.
|
|
225
|
+
|
|
226
|
+
### from_front_matter
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
RobotLab::Capabilities.from_front_matter(front_matter_hash) # => Capabilities
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Build from a parsed `SKILL.md` front-matter hash, reading `fs_read`, `fs_write`,
|
|
233
|
+
`network`, `timeout`, and `trust`. A `nil` front matter yields an all-defaults
|
|
234
|
+
instance.
|
|
235
|
+
|
|
236
|
+
### fm_value
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
RobotLab::Capabilities.fm_value(front_matter, :network, false)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Look up a front-matter key tolerating **either** string or symbol keys (string
|
|
243
|
+
first, then symbol), returning `default` when both are `nil`. Exposed because
|
|
244
|
+
`from_front_matter` uses it and skill-tooling may need the same leniency.
|
|
245
|
+
|
|
246
|
+
### ceiling
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
RobotLab::Capabilities.ceiling # => from RobotLab.config.sandbox
|
|
250
|
+
RobotLab::Capabilities.ceiling(custom_config)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
The maximum grant any skill may receive, read from the config's `sandbox:`
|
|
254
|
+
section. When there is no `sandbox` section at all, the ceiling is
|
|
255
|
+
`Capabilities.new(fs_read: ["."])` — read-only access to the working directory,
|
|
256
|
+
no writes, no network.
|
|
257
|
+
|
|
258
|
+
Note the ceiling never carries a `trust` — trust is a property of the skill, not
|
|
259
|
+
of the ceiling, and `intersect` keeps the declared value.
|
|
260
|
+
|
|
261
|
+
### core?
|
|
262
|
+
|
|
263
|
+
```ruby
|
|
264
|
+
capabilities.core? # => trust == "core"
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
A `core` skill is exempt from confinement: `Sandbox.for` returns a
|
|
268
|
+
[`Null`](#robotlabsandboxnull) strategy for it regardless of platform or config.
|
|
269
|
+
Reserve `trust: core` for bundles you wrote and audited.
|
|
270
|
+
|
|
271
|
+
### intersect
|
|
272
|
+
|
|
273
|
+
```ruby
|
|
274
|
+
grant = declared.intersect(RobotLab::Capabilities.ceiling)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
The effective grant. Per field:
|
|
278
|
+
|
|
279
|
+
| Field | Rule |
|
|
280
|
+
|-------|------|
|
|
281
|
+
| `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 |
|
|
282
|
+
| `network` | `declared && ceiling` — both must allow it |
|
|
283
|
+
| `timeout` | The **smaller** of the two |
|
|
284
|
+
| `trust` | The **declared** value, unchanged |
|
|
285
|
+
|
|
286
|
+
Because the check is prefix-based on expanded paths, a ceiling of `["."]` grants
|
|
287
|
+
nothing outside the working directory even if a skill asks for `/etc`.
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## RobotLab::ScriptTool
|
|
292
|
+
|
|
293
|
+
Factory module that turns an executable script into a `RobotLab::Tool`. All
|
|
294
|
+
methods are module functions.
|
|
295
|
+
|
|
296
|
+
### ScriptTool.from_path
|
|
297
|
+
|
|
298
|
+
```ruby
|
|
299
|
+
tool = RobotLab::ScriptTool.from_path(script_path, capabilities: nil, skill_dir: nil)
|
|
300
|
+
# => RobotLab::Tool, or nil
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
| Name | Type | Default | Description |
|
|
304
|
+
|------|------|---------|-------------|
|
|
305
|
+
| `script_path` | `String`, `Pathname` | **required** | The script file |
|
|
306
|
+
| `capabilities` | `Capabilities`, `nil` | `nil` → `Capabilities.new` | The skill's declared capabilities |
|
|
307
|
+
| `skill_dir` | `String`, `nil` | `nil` → the script's own directory | Bundle root; always granted read access under Seatbelt |
|
|
308
|
+
|
|
309
|
+
**Returns `nil`** when the file is not executable, logging
|
|
310
|
+
`"ScriptTool: <basename> is not executable, skipping"` at `warn`. It never raises.
|
|
311
|
+
|
|
312
|
+
The generated tool takes a single optional `args` string parameter, which is
|
|
313
|
+
`Shellwords.split` and appended to `bash <script>`. Its name comes from
|
|
314
|
+
[`derive_name`](#scripttoolderive_name) and its description from
|
|
315
|
+
[`extract_description`](#scripttoolextract_description).
|
|
316
|
+
|
|
317
|
+
### ScriptTool.execute
|
|
318
|
+
|
|
319
|
+
```ruby
|
|
320
|
+
RobotLab::ScriptTool.execute(cmd, capabilities:, skill_dir:) # => String
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Run a command array and return its combined stdout+stderr, or an error string.
|
|
324
|
+
Two paths:
|
|
325
|
+
|
|
326
|
+
- **Sandboxing off** (the default) — `Open3.capture2e`, unconfined, **no timeout**.
|
|
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.
|
|
332
|
+
|
|
333
|
+
### ScriptTool.run_with_timeout
|
|
334
|
+
|
|
335
|
+
```ruby
|
|
336
|
+
RobotLab::ScriptTool.run_with_timeout(cmd, timeout)
|
|
337
|
+
# => [String, Process::Status | nil]
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Run `cmd` in its own process group (`pgroup: true`), reading combined output
|
|
341
|
+
until `timeout` seconds elapse. On expiry it terminates the group and returns
|
|
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
|
+
```
|
|
350
|
+
|
|
351
|
+
`Process.kill('-TERM', ...)` against the process **group** of `pid`, so a script
|
|
352
|
+
that spawned children takes them down with it. Swallows every error and returns
|
|
353
|
+
`nil` — a process that already exited is not an error.
|
|
354
|
+
|
|
355
|
+
### ScriptTool.format_result
|
|
356
|
+
|
|
357
|
+
```ruby
|
|
358
|
+
RobotLab::ScriptTool.format_result(output, status) # => String
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
| `status` | Result |
|
|
362
|
+
|----------|--------|
|
|
363
|
+
| `nil` | `"Error (timed out):\n<output>"` |
|
|
364
|
+
| success | `output` verbatim |
|
|
365
|
+
| non-zero exit | `"Error (exit <N>):\n<output>"` |
|
|
366
|
+
|
|
367
|
+
Failures come back as **text for the LLM**, not exceptions — the model sees the
|
|
368
|
+
error and can adapt.
|
|
369
|
+
|
|
370
|
+
### ScriptTool.derive_name
|
|
371
|
+
|
|
372
|
+
```ruby
|
|
373
|
+
RobotLab::ScriptTool.derive_name(Pathname.new("check-deploy.sh")) # => "check_deploy"
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Strips the final extension, replaces every run of non-alphanumerics with `_`, and
|
|
377
|
+
trims leading/trailing underscores.
|
|
378
|
+
|
|
379
|
+
### ScriptTool.extract_description
|
|
380
|
+
|
|
381
|
+
```ruby
|
|
382
|
+
RobotLab::ScriptTool.extract_description(path) # => String
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
The first non-shebang comment line in the file, with leading `#` and whitespace
|
|
386
|
+
removed. Falls back to `derive_name(path)` when there is no comment or the file
|
|
387
|
+
cannot be read.
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
#!/usr/bin/env bash
|
|
391
|
+
# Verifies a deployment's health before promoting it. <- becomes the description
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## RobotLab::Sandbox
|
|
397
|
+
|
|
398
|
+
Strategy selector for confining skill-script execution. Module functions.
|
|
399
|
+
|
|
400
|
+
### Sandbox.enabled?
|
|
401
|
+
|
|
402
|
+
```ruby
|
|
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 |
|
|
429
|
+
|
|
430
|
+
Selection order: a `trust: core` grant gets `Null`; otherwise macOS gets
|
|
431
|
+
`Seatbelt`; anything else warns once and gets `Null`.
|
|
432
|
+
|
|
433
|
+
### Sandbox.warn_once_non_macos
|
|
434
|
+
|
|
435
|
+
```ruby
|
|
436
|
+
RobotLab::Sandbox.warn_once_non_macos
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
Emits `"Sandbox: OS-level confinement is only available on macOS; scripts run
|
|
440
|
+
unconfined here"` at `warn`, at most once per process. Idempotent, so a run with
|
|
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
|
+
```
|
|
491
|
+
|
|
492
|
+
The generated Seatbelt profile. Public so the policy can be asserted in tests
|
|
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`.
|
|
511
|
+
|
|
512
|
+
---
|
|
513
|
+
|
|
514
|
+
## See Also
|
|
515
|
+
|
|
516
|
+
- [Using Tools: Skill Scripts and Sandboxing](../guides/using-tools.md#skill-scripts-and-sandboxing)
|
|
517
|
+
- [Configuration: `sandbox:` section](../getting-started/configuration.md#skill-script-sandboxing-sandbox-section)
|
|
518
|
+
- [Robot: Skills](core/robot.md#skills) — template skills, the other meaning
|
|
519
|
+
- [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
|