robot_lab 0.2.8 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -0
  3. data/README.md +16 -18
  4. data/lib/robot_lab/ask_user.rb +3 -3
  5. data/lib/robot_lab/config/defaults.yml +5 -5
  6. data/lib/robot_lab/config.rb +2 -2
  7. data/lib/robot_lab/mcp/transports/streamable_http.rb +2 -4
  8. data/lib/robot_lab/memory.rb +16 -7
  9. data/lib/robot_lab/robot/hooking.rb +26 -0
  10. data/lib/robot_lab/robot/result_building.rb +119 -0
  11. data/lib/robot_lab/robot.rb +37 -120
  12. data/lib/robot_lab/run_config.rb +52 -20
  13. data/lib/robot_lab/tool.rb +7 -12
  14. data/lib/robot_lab/version.rb +1 -1
  15. data/lib/robot_lab.rb +3 -3
  16. metadata +15 -274
  17. data/.architecture/AGENTS.md +0 -32
  18. data/.architecture/config.yml +0 -8
  19. data/.architecture/members.yml +0 -60
  20. data/.architecture/reviews/feature-free-will.md +0 -490
  21. data/.architecture/reviews/overall-codebase.md +0 -427
  22. data/.claude/settings.local.json +0 -57
  23. data/.codex/config.toml +0 -2
  24. data/.envrc +0 -6
  25. data/.github/workflows/deploy-github-pages.yml +0 -52
  26. data/.irbrc +0 -6
  27. data/.loki +0 -65
  28. data/.quality/reek_baseline.txt +0 -43
  29. data/.rubocop.yml +0 -10
  30. data/Archspec.rb +0 -44
  31. data/CLAUDE.md +0 -139
  32. data/COMMITS.md +0 -196
  33. data/Rakefile +0 -193
  34. data/_typos.toml +0 -21
  35. data/agent2agent_review.md +0 -192
  36. data/agentf_improvements.md +0 -253
  37. data/agents.md +0 -14
  38. data/docs/api/core/index.md +0 -147
  39. data/docs/api/core/memory.md +0 -578
  40. data/docs/api/core/network.md +0 -553
  41. data/docs/api/core/result.md +0 -211
  42. data/docs/api/core/robot.md +0 -1667
  43. data/docs/api/core/state.md +0 -145
  44. data/docs/api/core/tool.md +0 -520
  45. data/docs/api/errors.md +0 -323
  46. data/docs/api/hooks.md +0 -469
  47. data/docs/api/index.md +0 -137
  48. data/docs/api/mcp/client.md +0 -387
  49. data/docs/api/mcp/index.md +0 -267
  50. data/docs/api/mcp/server.md +0 -252
  51. data/docs/api/mcp/transports.md +0 -339
  52. data/docs/api/messages/index.md +0 -126
  53. data/docs/api/messages/text-message.md +0 -170
  54. data/docs/api/messages/tool-call-message.md +0 -206
  55. data/docs/api/messages/tool-result-message.md +0 -242
  56. data/docs/api/messages/user-message.md +0 -232
  57. data/docs/api/skills.md +0 -423
  58. data/docs/api/streaming/context.md +0 -280
  59. data/docs/api/streaming/events.md +0 -185
  60. data/docs/api/streaming/index.md +0 -156
  61. data/docs/api/support.md +0 -846
  62. data/docs/architecture/core-concepts.md +0 -575
  63. data/docs/architecture/index.md +0 -252
  64. data/docs/architecture/message-flow.md +0 -366
  65. data/docs/architecture/network-orchestration.md +0 -478
  66. data/docs/architecture/robot-execution.md +0 -396
  67. data/docs/architecture/state-management.md +0 -410
  68. data/docs/assets/css/custom.css +0 -56
  69. data/docs/assets/images/robot_lab.jpg +0 -0
  70. data/docs/concepts.md +0 -641
  71. data/docs/examples/basic-chat.md +0 -258
  72. data/docs/examples/index.md +0 -425
  73. data/docs/examples/mcp-server.md +0 -403
  74. data/docs/examples/multi-robot-network.md +0 -476
  75. data/docs/examples/tool-usage.md +0 -468
  76. data/docs/getting-started/configuration.md +0 -645
  77. data/docs/getting-started/index.md +0 -56
  78. data/docs/getting-started/installation.md +0 -200
  79. data/docs/getting-started/quick-start.md +0 -293
  80. data/docs/guides/building-robots.md +0 -1187
  81. data/docs/guides/creating-networks.md +0 -736
  82. data/docs/guides/hooks.md +0 -1206
  83. data/docs/guides/index.md +0 -78
  84. data/docs/guides/knowledge.md +0 -219
  85. data/docs/guides/mcp-integration.md +0 -740
  86. data/docs/guides/memory.md +0 -458
  87. data/docs/guides/observability.md +0 -733
  88. data/docs/guides/streaming.md +0 -323
  89. data/docs/guides/using-tools.md +0 -780
  90. data/docs/index.md +0 -280
  91. data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
  92. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
  93. data/examples/.envrc +0 -1
  94. data/examples/01_simple_robot.rb +0 -31
  95. data/examples/02_tools.rb +0 -109
  96. data/examples/03_network.rb +0 -118
  97. data/examples/04_mcp.rb +0 -215
  98. data/examples/05_streaming.rb +0 -137
  99. data/examples/06_prompt_templates.rb +0 -325
  100. data/examples/07_network_memory.rb +0 -313
  101. data/examples/08_llm_config.rb +0 -162
  102. data/examples/09_chaining.rb +0 -251
  103. data/examples/10_memory.rb +0 -315
  104. data/examples/11_network_introspection.rb +0 -239
  105. data/examples/12_message_bus.rb +0 -67
  106. data/examples/13_spawn.rb +0 -85
  107. data/examples/14_rusty_circuit/.envrc +0 -1
  108. data/examples/14_rusty_circuit/.gitignore +0 -1
  109. data/examples/14_rusty_circuit/README.md +0 -1
  110. data/examples/14_rusty_circuit/comic.rb +0 -149
  111. data/examples/14_rusty_circuit/display.rb +0 -217
  112. data/examples/14_rusty_circuit/heckler.rb +0 -65
  113. data/examples/14_rusty_circuit/open_mic.rb +0 -132
  114. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
  115. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
  116. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
  117. data/examples/14_rusty_circuit/scout.rb +0 -165
  118. data/examples/14_rusty_circuit/show.log +0 -234
  119. data/examples/15_memory_network_and_bus/.envrc +0 -1
  120. data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
  121. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
  122. data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
  123. data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
  124. data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
  125. data/examples/15_memory_network_and_bus/output/combined_article.md +0 -13
  126. data/examples/15_memory_network_and_bus/output/final_article.md +0 -13
  127. data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
  128. data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
  129. data/examples/15_memory_network_and_bus/output/memory.json +0 -13
  130. data/examples/15_memory_network_and_bus/output/revision_1.md +0 -18
  131. data/examples/15_memory_network_and_bus/output/revision_2.md +0 -13
  132. data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
  133. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
  134. data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
  135. data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
  136. data/examples/16_writers_room/.envrc +0 -1
  137. data/examples/16_writers_room/display.rb +0 -158
  138. data/examples/16_writers_room/output/.gitignore +0 -4
  139. data/examples/16_writers_room/output/README.md +0 -69
  140. data/examples/16_writers_room/output/opus_001.md +0 -263
  141. data/examples/16_writers_room/output/opus_001_notes.log +0 -470
  142. data/examples/16_writers_room/output/opus_002.md +0 -245
  143. data/examples/16_writers_room/output/opus_002_notes.log +0 -546
  144. data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
  145. data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
  146. data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
  147. data/examples/16_writers_room/prompts/writer.md +0 -37
  148. data/examples/16_writers_room/room.rb +0 -186
  149. data/examples/16_writers_room/tools.rb +0 -173
  150. data/examples/16_writers_room/writer.rb +0 -121
  151. data/examples/16_writers_room/writers_room.rb +0 -256
  152. data/examples/17_skills.rb +0 -234
  153. data/examples/18_rails/.envrc +0 -3
  154. data/examples/18_rails/.gitignore +0 -5
  155. data/examples/18_rails/Gemfile +0 -11
  156. data/examples/18_rails/README.md +0 -67
  157. data/examples/18_rails/Rakefile +0 -4
  158. data/examples/18_rails/app/controllers/application_controller.rb +0 -4
  159. data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
  160. data/examples/18_rails/app/jobs/application_job.rb +0 -4
  161. data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
  162. data/examples/18_rails/app/models/application_record.rb +0 -5
  163. data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
  164. data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
  165. data/examples/18_rails/app/robots/chat_robot.rb +0 -25
  166. data/examples/18_rails/app/tools/time_tool.rb +0 -9
  167. data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
  168. data/examples/18_rails/app/views/chat/index.html.erb +0 -67
  169. data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
  170. data/examples/18_rails/bin/dev +0 -7
  171. data/examples/18_rails/bin/rails +0 -6
  172. data/examples/18_rails/bin/setup +0 -15
  173. data/examples/18_rails/config/application.rb +0 -33
  174. data/examples/18_rails/config/cable.yml +0 -2
  175. data/examples/18_rails/config/database.yml +0 -5
  176. data/examples/18_rails/config/environment.rb +0 -4
  177. data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
  178. data/examples/18_rails/config/routes.rb +0 -6
  179. data/examples/18_rails/config.ru +0 -4
  180. data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
  181. data/examples/19_token_tracking.rb +0 -138
  182. data/examples/20_circuit_breaker.rb +0 -147
  183. data/examples/21_learning_loop.rb +0 -181
  184. data/examples/22_context_compression.rb +0 -172
  185. data/examples/23_convergence.rb +0 -133
  186. data/examples/24_structured_delegation.rb +0 -151
  187. data/examples/25_history_search/conversation.jsonl +0 -30
  188. data/examples/25_history_search.rb +0 -133
  189. data/examples/26_document_store/api_versioning_adr.md +0 -52
  190. data/examples/26_document_store/incident_postmortem.md +0 -46
  191. data/examples/26_document_store/postgres_runbook.md +0 -49
  192. data/examples/26_document_store/redis_caching_guide.md +0 -48
  193. data/examples/26_document_store/sidekiq_guide.md +0 -51
  194. data/examples/26_document_store.rb +0 -140
  195. data/examples/27_incident_response/incident_response.rb +0 -261
  196. data/examples/28_mcp_discovery.rb +0 -113
  197. data/examples/29_ractor_tools.rb +0 -240
  198. data/examples/30_ractor_network.rb +0 -252
  199. data/examples/31_launch_assessment.rb +0 -246
  200. data/examples/32_newsletter_reader.rb +0 -193
  201. data/examples/33_stock_generator.rb +0 -80
  202. data/examples/33_stock_predictor.rb +0 -327
  203. data/examples/34_agentskills.rb +0 -76
  204. data/examples/35_hooks.rb +0 -266
  205. data/examples/README.md +0 -533
  206. data/examples/common.rb +0 -148
  207. data/examples/prompts/assistant.md +0 -5
  208. data/examples/prompts/audit_trail.md +0 -8
  209. data/examples/prompts/billing.md +0 -10
  210. data/examples/prompts/classifier.md +0 -7
  211. data/examples/prompts/comedian.md +0 -6
  212. data/examples/prompts/comedy_critic.md +0 -10
  213. data/examples/prompts/configurable.md +0 -9
  214. data/examples/prompts/dispatcher.md +0 -12
  215. data/examples/prompts/entity_extractor.md +0 -14
  216. data/examples/prompts/escalation.md +0 -42
  217. data/examples/prompts/frontmatter_mcp_test.md +0 -9
  218. data/examples/prompts/frontmatter_named_test.md +0 -5
  219. data/examples/prompts/frontmatter_tools_test.md +0 -6
  220. data/examples/prompts/general.md +0 -7
  221. data/examples/prompts/github_assistant.md +0 -14
  222. data/examples/prompts/helper.md +0 -4
  223. data/examples/prompts/incident_responder.md +0 -18
  224. data/examples/prompts/keyword_extractor.md +0 -11
  225. data/examples/prompts/llm_config_demo.md +0 -20
  226. data/examples/prompts/order_support.md +0 -35
  227. data/examples/prompts/os_advocate.md +0 -13
  228. data/examples/prompts/os_chief.md +0 -13
  229. data/examples/prompts/os_editor.md +0 -13
  230. data/examples/prompts/parameterized_main_test.md +0 -6
  231. data/examples/prompts/pii_redactor.md +0 -7
  232. data/examples/prompts/product_support.md +0 -37
  233. data/examples/prompts/runbook_protocol.md +0 -12
  234. data/examples/prompts/sentiment_analyzer.md +0 -12
  235. data/examples/prompts/skill_a_test.md +0 -4
  236. data/examples/prompts/skill_b_test.md +0 -4
  237. data/examples/prompts/skill_config_test.md +0 -5
  238. data/examples/prompts/skill_cycle_a_test.md +0 -6
  239. data/examples/prompts/skill_cycle_b_test.md +0 -6
  240. data/examples/prompts/skill_description_test.md +0 -4
  241. data/examples/prompts/skill_leaf_test.md +0 -4
  242. data/examples/prompts/skill_nested_test.md +0 -6
  243. data/examples/prompts/skill_refs_main_test.md +0 -6
  244. data/examples/prompts/skill_self_ref_test.md +0 -6
  245. data/examples/prompts/skill_with_mcp_test.md +0 -9
  246. data/examples/prompts/skill_with_params_test.md +0 -6
  247. data/examples/prompts/skill_with_robot_name_test.md +0 -5
  248. data/examples/prompts/skill_with_tools_test.md +0 -6
  249. data/examples/prompts/sre_compliance.md +0 -10
  250. data/examples/prompts/structured_output.md +0 -7
  251. data/examples/prompts/synthesizer.md +0 -17
  252. data/examples/prompts/technical.md +0 -10
  253. data/examples/prompts/template_with_skills_test.md +0 -6
  254. data/examples/prompts/triage.md +0 -22
  255. data/examples/ruboruby.md +0 -423
  256. data/examples/xyzzy.rb +0 -97
  257. data/logfile +0 -8
  258. data/mkdocs.yml +0 -209
  259. data/robot_concurrency.md +0 -38
  260. data/simple_acp_review.md +0 -298
  261. data/temp.md +0 -6
  262. data/tool_manifest_plan.md +0 -155
data/docs/api/skills.md DELETED
@@ -1,423 +0,0 @@
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
@@ -1,280 +0,0 @@
1
- # Streaming::Context
2
-
3
- Manages streaming event publishing with automatic sequencing, timestamping, and ID generation.
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
-
11
- ## Class: `RobotLab::Streaming::Context`
12
-
13
- ```ruby
14
- context = RobotLab::Streaming::Context.new(
15
- run_id: "run_123",
16
- message_id: "msg_456",
17
- scope: "network",
18
- publish: ->(event) { broadcast(event) }
19
- )
20
-
21
- context.publish_event(event: "text.delta", data: { delta: "Hello" })
22
- ```
23
-
24
- ## Constructor
25
-
26
- ```ruby
27
- Context.new(
28
- run_id:,
29
- message_id:,
30
- scope:,
31
- publish:,
32
- parent_run_id: nil,
33
- sequence_counter: nil
34
- )
35
- ```
36
-
37
- **Parameters:**
38
-
39
- | Name | Type | Default | Description |
40
- |------|------|---------|-------------|
41
- | `run_id` | `String` | required | Unique identifier for this run |
42
- | `message_id` | `String` | required | Current message identifier |
43
- | `scope` | `String`, `Symbol` | required | Context scope (e.g., `"network"`, `"robot"`) |
44
- | `publish` | `Proc` | required | Callback invoked with each event hash |
45
- | `parent_run_id` | `String`, `nil` | `nil` | Parent run identifier for nested contexts |
46
- | `sequence_counter` | `SequenceCounter`, `nil` | `nil` | Shared sequence counter (creates new one if nil) |
47
-
48
- ## Attributes
49
-
50
- ### run_id
51
-
52
- ```ruby
53
- context.run_id # => String
54
- ```
55
-
56
- The unique run identifier for this context.
57
-
58
- ### parent_run_id
59
-
60
- ```ruby
61
- context.parent_run_id # => String | nil
62
- ```
63
-
64
- The parent run identifier. Set when this is a child context created by `create_child_context`.
65
-
66
- ### message_id
67
-
68
- ```ruby
69
- context.message_id # => String
70
- ```
71
-
72
- The current message identifier.
73
-
74
- ### scope
75
-
76
- ```ruby
77
- context.scope # => String
78
- ```
79
-
80
- The context scope (converted to string). Typically `"network"` or `"robot"`.
81
-
82
- ## Methods
83
-
84
- ### publish_event
85
-
86
- ```ruby
87
- chunk = context.publish_event(event:, data: {})
88
- ```
89
-
90
- Publish a streaming event. The event is wrapped in a chunk with automatic sequencing, timestamping, and context metadata injection.
91
-
92
- **Parameters:**
93
-
94
- | Name | Type | Description |
95
- |------|------|-------------|
96
- | `event` | `String` | Event type (e.g., `"text.delta"`, `"run.started"`) |
97
- | `data` | `Hash` | Event payload (default: `{}`) |
98
-
99
- **Returns:** The constructed event chunk hash.
100
-
101
- The chunk structure:
102
-
103
- ```ruby
104
- {
105
- event: "text.delta",
106
- data: {
107
- delta: "Hello", # from data parameter
108
- run_id: "run_123", # injected from context
109
- message_id: "msg_456", # injected from context
110
- scope: "robot" # injected from context
111
- },
112
- timestamp: 1707900000000, # millisecond Unix timestamp
113
- sequence_number: 1, # monotonically increasing
114
- id: "publish-1:text.delta" # unique event ID
115
- }
116
- ```
117
-
118
- If the publish callback raises an error, it is caught and logged as a warning via `RobotLab.config.logger`. The chunk is still returned.
119
-
120
- ### create_child_context
121
-
122
- ```ruby
123
- child = context.create_child_context(robot_run_id)
124
- ```
125
-
126
- Create a child context for a nested robot execution. The child shares the same publish callback and sequence counter, ensuring events are ordered globally across the parent and child.
127
-
128
- **Parameters:**
129
-
130
- | Name | Type | Description |
131
- |------|------|-------------|
132
- | `robot_run_id` | `String` | Run ID for the child context |
133
-
134
- **Returns:** A new `Context` with:
135
- - `run_id` set to `robot_run_id`
136
- - `parent_run_id` set to the current context's `run_id`
137
- - `scope` set to `"robot"`
138
- - A new `message_id` (generated UUID)
139
- - Shared `sequence_counter` and `publish` callback
140
-
141
- ### create_context_with_shared_sequence
142
-
143
- ```ruby
144
- sibling = context.create_context_with_shared_sequence(
145
- run_id: "run_789",
146
- message_id: "msg_789",
147
- scope: "robot"
148
- )
149
- ```
150
-
151
- Create a new context that shares the same sequence counter as this context, but with different identifiers.
152
-
153
- **Parameters:**
154
-
155
- | Name | Type | Description |
156
- |------|------|-------------|
157
- | `run_id` | `String` | Run ID for the new context |
158
- | `message_id` | `String` | Message ID for the new context |
159
- | `scope` | `String` | Scope for the new context |
160
-
161
- **Returns:** A new `Context` sharing the sequence counter and publish callback.
162
-
163
- ### generate_part_id
164
-
165
- ```ruby
166
- context.generate_part_id # => "part_37ac1d49_815007_8f522876"
167
- ```
168
-
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.
180
-
181
- ### generate_step_id
182
-
183
- ```ruby
184
- context.generate_step_id("text_output") # => "publish-3:text_output"
185
- ```
186
-
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.
192
-
193
- **Parameters:**
194
-
195
- | Name | Type | Description |
196
- |------|------|-------------|
197
- | `base_name` | `String` | Base name for the step |
198
-
199
- ### generate_message_id
200
-
201
- ```ruby
202
- context.generate_message_id # => "a1b2c3d4-..."
203
- ```
204
-
205
- Generate a new UUID for use as a message identifier.
206
-
207
- ## Examples
208
-
209
- ### Basic Event Publishing
210
-
211
- ```ruby
212
- publish = ->(event) {
213
- puts "[#{event[:event]}] #{event[:data]}"
214
- }
215
-
216
- context = RobotLab::Streaming::Context.new(
217
- run_id: SecureRandom.uuid,
218
- message_id: SecureRandom.uuid,
219
- scope: "robot",
220
- publish: publish
221
- )
222
-
223
- context.publish_event(event: "run.started", data: { robot_name: "assistant" })
224
- context.publish_event(event: "text.delta", data: { delta: "Hello " })
225
- context.publish_event(event: "text.delta", data: { delta: "world!" })
226
- context.publish_event(event: "run.completed", data: {})
227
- ```
228
-
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.
233
-
234
- ```ruby
235
- # Parent context
236
- network_ctx = RobotLab::Streaming::Context.new(
237
- run_id: "net_run_1",
238
- message_id: "net_msg_1",
239
- scope: "network",
240
- publish: ->(e) { stream_to_client(e) }
241
- )
242
-
243
- network_ctx.publish_event(event: "run.started", data: {})
244
-
245
- # Robot 1 executes
246
- robot1_ctx = network_ctx.create_child_context("robot1_run_1")
247
- robot1_ctx.publish_event(event: "step.started", data: { robot: "classifier" })
248
- robot1_ctx.publish_event(event: "text.delta", data: { delta: "Category: billing" })
249
- robot1_ctx.publish_event(event: "step.completed", data: { robot: "classifier" })
250
-
251
- # Robot 2 executes (sequence numbers continue from robot 1)
252
- robot2_ctx = network_ctx.create_child_context("robot2_run_1")
253
- robot2_ctx.publish_event(event: "step.started", data: { robot: "responder" })
254
- robot2_ctx.publish_event(event: "text.delta", data: { delta: "I can help with that." })
255
- robot2_ctx.publish_event(event: "step.completed", data: { robot: "responder" })
256
-
257
- network_ctx.publish_event(event: "run.completed", data: {})
258
- ```
259
-
260
- ### Error-Safe Publishing
261
-
262
- ```ruby
263
- # Errors in the publish callback are caught and logged,
264
- # so streaming failures do not interrupt execution.
265
- context = RobotLab::Streaming::Context.new(
266
- run_id: "run_1",
267
- message_id: "msg_1",
268
- scope: "robot",
269
- publish: ->(e) { raise "connection lost" }
270
- )
271
-
272
- # This does not raise -- the error is logged via RobotLab.config.logger
273
- chunk = context.publish_event(event: "text.delta", data: { delta: "test" })
274
- # chunk is still returned with the event data
275
- ```
276
-
277
- ## See Also
278
-
279
- - [Streaming Overview](index.md)
280
- - [Events](events.md)