robot_lab 0.1.0 → 0.2.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 (242) hide show
  1. checksums.yaml +4 -4
  2. data/.architecture/AGENTS.md +32 -0
  3. data/.architecture/config.yml +8 -0
  4. data/.architecture/members.yml +60 -0
  5. data/.architecture/reviews/feature-free-will.md +490 -0
  6. data/.architecture/reviews/overall-codebase.md +427 -0
  7. data/.claude/settings.local.json +57 -0
  8. data/.codex/config.toml +2 -0
  9. data/.irbrc +2 -2
  10. data/.rubocop.yml +172 -0
  11. data/CHANGELOG.md +72 -0
  12. data/CLAUDE.md +139 -0
  13. data/README.md +91 -95
  14. data/Rakefile +109 -3
  15. data/agent2agent_review.md +192 -0
  16. data/agentf_improvements.md +253 -0
  17. data/agents.md +14 -0
  18. data/docs/examples/index.md +37 -2
  19. data/docs/getting-started/configuration.md +20 -7
  20. data/docs/guides/index.md +16 -16
  21. data/docs/guides/knowledge.md +7 -1
  22. data/docs/guides/observability.md +132 -0
  23. data/docs/index.md +30 -3
  24. data/docs/superpowers/plans/2026-05-06-agentskills.md +1303 -0
  25. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +247 -0
  26. data/examples/.envrc +1 -0
  27. data/examples/01_simple_robot.rb +5 -9
  28. data/examples/02_tools.rb +5 -9
  29. data/examples/03_network.rb +8 -9
  30. data/examples/04_mcp.rb +21 -29
  31. data/examples/05_streaming.rb +12 -18
  32. data/examples/06_prompt_templates.rb +11 -19
  33. data/examples/07_network_memory.rb +16 -31
  34. data/examples/08_llm_config.rb +10 -22
  35. data/examples/09_chaining.rb +16 -27
  36. data/examples/10_memory.rb +12 -28
  37. data/examples/11_network_introspection.rb +15 -29
  38. data/examples/12_message_bus.rb +5 -12
  39. data/examples/13_spawn.rb +5 -10
  40. data/examples/14_rusty_circuit/.envrc +1 -0
  41. data/examples/14_rusty_circuit/comic.rb +2 -0
  42. data/examples/14_rusty_circuit/heckler.rb +1 -1
  43. data/examples/14_rusty_circuit/open_mic.rb +1 -3
  44. data/examples/14_rusty_circuit/scout.rb +2 -0
  45. data/examples/15_memory_network_and_bus/.envrc +1 -0
  46. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +6 -3
  47. data/examples/15_memory_network_and_bus/linux_writer.rb +1 -1
  48. data/examples/15_memory_network_and_bus/output/combined_article.md +6 -6
  49. data/examples/15_memory_network_and_bus/output/final_article.md +6 -8
  50. data/examples/15_memory_network_and_bus/output/linux_draft.md +4 -2
  51. data/examples/15_memory_network_and_bus/output/mac_draft.md +3 -3
  52. data/examples/15_memory_network_and_bus/output/memory.json +6 -6
  53. data/examples/15_memory_network_and_bus/output/revision_1.md +10 -11
  54. data/examples/15_memory_network_and_bus/output/revision_2.md +6 -8
  55. data/examples/15_memory_network_and_bus/output/windows_draft.md +3 -3
  56. data/examples/16_writers_room/.envrc +1 -0
  57. data/examples/16_writers_room/writers_room.rb +2 -4
  58. data/examples/17_skills.rb +8 -17
  59. data/examples/18_rails/Gemfile +1 -0
  60. data/examples/19_token_tracking.rb +9 -15
  61. data/examples/20_circuit_breaker.rb +10 -19
  62. data/examples/21_learning_loop.rb +11 -20
  63. data/examples/22_context_compression.rb +6 -13
  64. data/examples/23_convergence.rb +6 -17
  65. data/examples/24_structured_delegation.rb +11 -15
  66. data/examples/25_history_search.rb +5 -12
  67. data/examples/26_document_store.rb +6 -13
  68. data/examples/27_incident_response/incident_response.rb +4 -5
  69. data/examples/28_mcp_discovery.rb +8 -11
  70. data/examples/29_ractor_tools.rb +4 -9
  71. data/examples/30_ractor_network.rb +10 -19
  72. data/examples/31_launch_assessment.rb +10 -23
  73. data/examples/32_newsletter_reader.rb +188 -0
  74. data/examples/33_stock_generator.rb +80 -0
  75. data/examples/33_stock_predictor.rb +306 -0
  76. data/examples/34_agentskills.rb +72 -0
  77. data/examples/README.md +1 -1
  78. data/examples/common.rb +76 -0
  79. data/examples/ruboruby.md +423 -0
  80. data/examples/temp.md +51 -0
  81. data/lib/robot_lab/agent_skill.rb +63 -0
  82. data/lib/robot_lab/agent_skill_catalog.rb +74 -0
  83. data/lib/robot_lab/ask_user.rb +2 -2
  84. data/lib/robot_lab/bus_poller.rb +12 -5
  85. data/lib/robot_lab/config.rb +1 -12
  86. data/lib/robot_lab/delegation_future.rb +1 -1
  87. data/lib/robot_lab/doom_loop_detector.rb +98 -0
  88. data/lib/robot_lab/history_compressor.rb +4 -10
  89. data/lib/robot_lab/mcp/client.rb +1 -2
  90. data/lib/robot_lab/mcp/connection_poller.rb +3 -3
  91. data/lib/robot_lab/mcp/server.rb +1 -1
  92. data/lib/robot_lab/mcp/server_discovery.rb +0 -2
  93. data/lib/robot_lab/memory.rb +32 -27
  94. data/lib/robot_lab/memory_change.rb +2 -2
  95. data/lib/robot_lab/message.rb +4 -4
  96. data/lib/robot_lab/network.rb +11 -6
  97. data/lib/robot_lab/robot/agent_skill_matching.rb +99 -0
  98. data/lib/robot_lab/robot/bus_messaging.rb +9 -27
  99. data/lib/robot_lab/robot/history_search.rb +4 -1
  100. data/lib/robot_lab/robot/mcp_management.rb +5 -11
  101. data/lib/robot_lab/robot/template_rendering.rb +60 -40
  102. data/lib/robot_lab/robot.rb +323 -206
  103. data/lib/robot_lab/robot_result.rb +6 -5
  104. data/lib/robot_lab/run_config.rb +5 -11
  105. data/lib/robot_lab/script_tool.rb +76 -0
  106. data/lib/robot_lab/state_proxy.rb +7 -5
  107. data/lib/robot_lab/tool.rb +3 -3
  108. data/lib/robot_lab/tool_config.rb +1 -1
  109. data/lib/robot_lab/tool_manifest.rb +5 -7
  110. data/lib/robot_lab/user_message.rb +2 -2
  111. data/lib/robot_lab/version.rb +1 -1
  112. data/lib/robot_lab/waiter.rb +1 -1
  113. data/lib/robot_lab.rb +41 -52
  114. data/logfile +8 -0
  115. data/mkdocs.yml +2 -3
  116. data/robot_concurrency.md +38 -0
  117. data/simple_acp_review.md +298 -0
  118. data/site/404.html +2300 -0
  119. data/site/api/core/index.html +2706 -0
  120. data/site/api/core/memory/index.html +3793 -0
  121. data/site/api/core/network/index.html +3500 -0
  122. data/site/api/core/robot/index.html +4566 -0
  123. data/site/api/core/state/index.html +3390 -0
  124. data/site/api/core/tool/index.html +3843 -0
  125. data/site/api/index.html +2635 -0
  126. data/site/api/mcp/client/index.html +3435 -0
  127. data/site/api/mcp/index.html +2783 -0
  128. data/site/api/mcp/server/index.html +3252 -0
  129. data/site/api/mcp/transports/index.html +3352 -0
  130. data/site/api/messages/index.html +2641 -0
  131. data/site/api/messages/text-message/index.html +3087 -0
  132. data/site/api/messages/tool-call-message/index.html +3159 -0
  133. data/site/api/messages/tool-result-message/index.html +3252 -0
  134. data/site/api/messages/user-message/index.html +3212 -0
  135. data/site/api/streaming/context/index.html +3282 -0
  136. data/site/api/streaming/events/index.html +3347 -0
  137. data/site/api/streaming/index.html +2738 -0
  138. data/site/architecture/core-concepts/index.html +3757 -0
  139. data/site/architecture/index.html +2797 -0
  140. data/site/architecture/message-flow/index.html +3238 -0
  141. data/site/architecture/network-orchestration/index.html +3433 -0
  142. data/site/architecture/robot-execution/index.html +3140 -0
  143. data/site/architecture/state-management/index.html +3498 -0
  144. data/site/assets/css/custom.css +56 -0
  145. data/site/assets/images/favicon.png +0 -0
  146. data/site/assets/images/robot_lab.jpg +0 -0
  147. data/site/assets/javascripts/bundle.79ae519e.min.js +16 -0
  148. data/site/assets/javascripts/bundle.79ae519e.min.js.map +7 -0
  149. data/site/assets/javascripts/lunr/min/lunr.ar.min.js +1 -0
  150. data/site/assets/javascripts/lunr/min/lunr.da.min.js +18 -0
  151. data/site/assets/javascripts/lunr/min/lunr.de.min.js +18 -0
  152. data/site/assets/javascripts/lunr/min/lunr.du.min.js +18 -0
  153. data/site/assets/javascripts/lunr/min/lunr.el.min.js +1 -0
  154. data/site/assets/javascripts/lunr/min/lunr.es.min.js +18 -0
  155. data/site/assets/javascripts/lunr/min/lunr.fi.min.js +18 -0
  156. data/site/assets/javascripts/lunr/min/lunr.fr.min.js +18 -0
  157. data/site/assets/javascripts/lunr/min/lunr.he.min.js +1 -0
  158. data/site/assets/javascripts/lunr/min/lunr.hi.min.js +1 -0
  159. data/site/assets/javascripts/lunr/min/lunr.hu.min.js +18 -0
  160. data/site/assets/javascripts/lunr/min/lunr.hy.min.js +1 -0
  161. data/site/assets/javascripts/lunr/min/lunr.it.min.js +18 -0
  162. data/site/assets/javascripts/lunr/min/lunr.ja.min.js +1 -0
  163. data/site/assets/javascripts/lunr/min/lunr.jp.min.js +1 -0
  164. data/site/assets/javascripts/lunr/min/lunr.kn.min.js +1 -0
  165. data/site/assets/javascripts/lunr/min/lunr.ko.min.js +1 -0
  166. data/site/assets/javascripts/lunr/min/lunr.multi.min.js +1 -0
  167. data/site/assets/javascripts/lunr/min/lunr.nl.min.js +18 -0
  168. data/site/assets/javascripts/lunr/min/lunr.no.min.js +18 -0
  169. data/site/assets/javascripts/lunr/min/lunr.pt.min.js +18 -0
  170. data/site/assets/javascripts/lunr/min/lunr.ro.min.js +18 -0
  171. data/site/assets/javascripts/lunr/min/lunr.ru.min.js +18 -0
  172. data/site/assets/javascripts/lunr/min/lunr.sa.min.js +1 -0
  173. data/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +1 -0
  174. data/site/assets/javascripts/lunr/min/lunr.sv.min.js +18 -0
  175. data/site/assets/javascripts/lunr/min/lunr.ta.min.js +1 -0
  176. data/site/assets/javascripts/lunr/min/lunr.te.min.js +1 -0
  177. data/site/assets/javascripts/lunr/min/lunr.th.min.js +1 -0
  178. data/site/assets/javascripts/lunr/min/lunr.tr.min.js +18 -0
  179. data/site/assets/javascripts/lunr/min/lunr.vi.min.js +1 -0
  180. data/site/assets/javascripts/lunr/min/lunr.zh.min.js +1 -0
  181. data/site/assets/javascripts/lunr/tinyseg.js +206 -0
  182. data/site/assets/javascripts/lunr/wordcut.js +6708 -0
  183. data/site/assets/javascripts/workers/search.2c215733.min.js +42 -0
  184. data/site/assets/javascripts/workers/search.2c215733.min.js.map +7 -0
  185. data/site/assets/stylesheets/main.484c7ddc.min.css +1 -0
  186. data/site/assets/stylesheets/main.484c7ddc.min.css.map +1 -0
  187. data/site/assets/stylesheets/palette.ab4e12ef.min.css +1 -0
  188. data/site/assets/stylesheets/palette.ab4e12ef.min.css.map +1 -0
  189. data/site/concepts/index.html +3455 -0
  190. data/site/examples/basic-chat/index.html +2880 -0
  191. data/site/examples/index.html +2907 -0
  192. data/site/examples/mcp-server/index.html +3018 -0
  193. data/site/examples/multi-robot-network/index.html +3131 -0
  194. data/site/examples/rails-application/index.html +3329 -0
  195. data/site/examples/tool-usage/index.html +3085 -0
  196. data/site/getting-started/configuration/index.html +3745 -0
  197. data/site/getting-started/index.html +2572 -0
  198. data/site/getting-started/installation/index.html +2981 -0
  199. data/site/getting-started/quick-start/index.html +2942 -0
  200. data/site/guides/building-robots/index.html +4290 -0
  201. data/site/guides/creating-networks/index.html +3858 -0
  202. data/site/guides/index.html +2586 -0
  203. data/site/guides/mcp-integration/index.html +3581 -0
  204. data/site/guides/memory/index.html +3586 -0
  205. data/site/guides/rails-integration/index.html +4019 -0
  206. data/site/guides/streaming/index.html +3157 -0
  207. data/site/guides/using-tools/index.html +3802 -0
  208. data/site/index.html +2671 -0
  209. data/site/search/search_index.json +1 -0
  210. data/site/sitemap.xml +183 -0
  211. data/site/sitemap.xml.gz +0 -0
  212. data/site/tags.json +1 -0
  213. data/temp.md +6 -0
  214. data/tool_manifest_plan.md +155 -0
  215. metadata +154 -92
  216. data/docs/examples/rails-application.md +0 -419
  217. data/docs/guides/ractor-parallelism.md +0 -364
  218. data/docs/guides/rails-integration.md +0 -681
  219. data/docs/superpowers/plans/2026-04-14-ractor-integration.md +0 -1538
  220. data/docs/superpowers/specs/2026-04-14-ractor-integration-design.md +0 -258
  221. data/lib/generators/robot_lab/install_generator.rb +0 -90
  222. data/lib/generators/robot_lab/job_generator.rb +0 -40
  223. data/lib/generators/robot_lab/robot_generator.rb +0 -55
  224. data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -42
  225. data/lib/generators/robot_lab/templates/job.rb.tt +0 -21
  226. data/lib/generators/robot_lab/templates/migration.rb.tt +0 -32
  227. data/lib/generators/robot_lab/templates/result_model.rb.tt +0 -52
  228. data/lib/generators/robot_lab/templates/robot.rb.tt +0 -31
  229. data/lib/generators/robot_lab/templates/robot_job.rb.tt +0 -18
  230. data/lib/generators/robot_lab/templates/robot_test.rb.tt +0 -34
  231. data/lib/generators/robot_lab/templates/routing_robot.rb.tt +0 -59
  232. data/lib/generators/robot_lab/templates/thread_model.rb.tt +0 -40
  233. data/lib/robot_lab/document_store.rb +0 -155
  234. data/lib/robot_lab/ractor_boundary.rb +0 -42
  235. data/lib/robot_lab/ractor_job.rb +0 -37
  236. data/lib/robot_lab/ractor_memory_proxy.rb +0 -85
  237. data/lib/robot_lab/ractor_network_scheduler.rb +0 -154
  238. data/lib/robot_lab/ractor_worker_pool.rb +0 -117
  239. data/lib/robot_lab/rails_integration/engine.rb +0 -29
  240. data/lib/robot_lab/rails_integration/job.rb +0 -158
  241. data/lib/robot_lab/rails_integration/railtie.rb +0 -51
  242. data/lib/robot_lab/rails_integration/turbo_stream_callbacks.rb +0 -72
@@ -0,0 +1,247 @@
1
+ # AgentSkills.io Support — Design Spec
2
+
3
+ **Date:** 2026-05-06
4
+ **Status:** Approved
5
+
6
+ ## Overview
7
+
8
+ Add support for the [AgentSkills.io](https://agentskills.io) open standard to RobotLab's existing skills system. Skills are currently single `.md` prompt_manager template files referenced by symbol ID. This spec extends `skills:` to also recognize the AgentSkills folder format (`~/.prompts/skills/<name>/SKILL.md`) with bundled scripts auto-exposed as tools and runtime embedding-based matching for progressive disclosure.
9
+
10
+ ## Goals
11
+
12
+ - Unified `skills:` API — no new constructor params; format detected automatically
13
+ - AgentSkills folder skills use runtime embedding similarity to decide injection per `run()` call
14
+ - Scripts in `scripts/` auto-wrapped as `RobotLab::Tool` subclasses
15
+ - Zero breakage to existing PM template skills behavior
16
+ - Graceful degradation when embeddings fail or no skills match
17
+
18
+ ## Non-Goals
19
+
20
+ - Automatic catalog-wide skill discovery without explicit `skills:` listing
21
+ - Fetching remote skills from registries
22
+ - Skills referencing `references/` or `assets/` sub-folders (deferred)
23
+ - Modifying the AgentSkills.io specification
24
+
25
+ ---
26
+
27
+ ## Discovery Path
28
+
29
+ AgentSkill folders are resolved from a single fixed root: `~/.prompts/skills/`. Given `skills: [:check_style]`, the loader checks `~/.prompts/skills/check_style/SKILL.md`. If that file exists, it is an AgentSkill. If not, the existing PM template resolution applies.
30
+
31
+ This root is not configurable in v1.
32
+
33
+ ---
34
+
35
+ ## New Classes
36
+
37
+ ### `RobotLab::AgentSkill`
38
+
39
+ Plain Ruby value object. Constructed by the catalog or the expand_skills loader.
40
+
41
+ ```
42
+ AgentSkill
43
+ name: String # from SKILL.md front matter
44
+ description: String # from SKILL.md front matter — used for embedding
45
+ path: Pathname # directory path
46
+ instructions: String # lazy: SKILL.md body — everything after the closing ---
47
+ scripts: Array<Pathname> # lazy: glob of scripts/*
48
+ ```
49
+
50
+ - `instructions` and `scripts` are loaded on first access (lazy).
51
+ - Raises `RobotLab::ConfigurationError` on construction if `SKILL.md` is missing `name` or `description`.
52
+ - `description_vector` — memoized fastembed **passage** embedding of `description` (`DocumentStore#passage_vector`), computed on first match attempt.
53
+
54
+ ### `RobotLab::AgentSkillCatalog`
55
+
56
+ Module-level singleton (`RobotLab::AgentSkillCatalog`). Scans `~/.prompts/skills/` at process start (lazy, on first access). Caches `AgentSkill` objects keyed by name symbol.
57
+
58
+ Responsibilities:
59
+ - `find(id)` — return `AgentSkill` for a given symbol, or `nil`
60
+ - `all` — return all discovered skills
61
+ - Internal: compute and cache description vectors using the `DocumentStore` fastembed infrastructure
62
+
63
+ ### `RobotLab::AgentSkillMatching`
64
+
65
+ Module included in `Robot`. Contains the `run()` override and the similarity logic.
66
+
67
+ ### `RobotLab::ScriptTool`
68
+
69
+ Factory method `ScriptTool.from_path(path)` returns an anonymous `RobotLab::Tool` subclass. The tool:
70
+ - Name: script filename without extension, underscored (e.g. `check_style`)
71
+ - Description: first comment line of the script, or `"Run #{name}"`
72
+ - Execution: shells out via `Open3.capture2e`, returns stdout+stderr as the tool result
73
+ - Raises `RobotLab::ToolNotFoundError` if the script is not executable
74
+
75
+ ---
76
+
77
+ ## Modified Code
78
+
79
+ ### `Robot::TemplateRendering#expand_skills`
80
+
81
+ Before resolving a skill ID as a PM template, check `AgentSkillCatalog.find(skill_id)`:
82
+
83
+ ```
84
+ expand_skills([:runbook_protocol, :check_style], visited)
85
+ :runbook_protocol → catalog.find → nil → PM template (existing path)
86
+ :check_style → catalog.find → AgentSkill<check_style>
87
+ → store in @pending_agent_skills
88
+ → do NOT add to @expanded_skills
89
+ ```
90
+
91
+ `@expanded_skills` continues to hold only PM-format skill IDs, preserving all existing behavior.
92
+
93
+ ### `Robot` constructor
94
+
95
+ Add `@pending_agent_skills = []` alongside `@expanded_skills = nil`.
96
+
97
+ ### `Robot::AgentSkillMatching#run`
98
+
99
+ Prepended to `Robot` to intercept `run()`:
100
+
101
+ ```ruby
102
+ def run(message, **kwargs)
103
+ activated = match_agent_skills(message)
104
+ inject_agent_skills(activated)
105
+ super
106
+ ensure
107
+ restore_after_agent_skills(activated)
108
+ end
109
+ ```
110
+
111
+ **`match_agent_skills(message)`**
112
+ 1. Return `[]` if `@pending_agent_skills` is empty
113
+ 2. Embed `message` as a **query** vector (`DocumentStore#query_vector`) — distinct from the passage embedding used for skill descriptions
114
+ 3. For each pending AgentSkill, compute cosine similarity between message query-vector and `skill.description_vector` (passage-vector)
115
+ 4. Return skills where similarity >= `SIMILARITY_THRESHOLD` (default: `0.70`)
116
+
117
+ **`inject_agent_skills(skills)`**
118
+ 1. Prepend each skill's `instructions` to the chat's system prompt (using `with_instructions`)
119
+ 2. Instantiate `ScriptTool` for each script in each skill's `scripts/`; add to `@local_tools`
120
+
121
+ **`restore_after_agent_skills(skills)`**
122
+ 1. Remove injected script tools from `@local_tools`
123
+ 2. Restore system prompt to pre-injection state
124
+
125
+ The system prompt is snapshotted before injection and restored in the `ensure` block so state does not accumulate across calls.
126
+
127
+ ---
128
+
129
+ ## SKILL.md Format
130
+
131
+ Follows the AgentSkills.io specification. Front matter is YAML; body is Markdown.
132
+
133
+ ```markdown
134
+ ---
135
+ name: check_style
136
+ description: Review Ruby code style against project conventions
137
+ ---
138
+ When reviewing code, check for:
139
+ - Frozen string literal comments
140
+ - Method length under 20 lines
141
+ - No inline rescue
142
+ ```
143
+
144
+ RobotLab reads only `name` and `description` from front matter. All other front matter keys are ignored (no LLM config via SKILL.md — that remains the PM template system's domain).
145
+
146
+ ---
147
+
148
+ ## Script Tools
149
+
150
+ Given `~/.prompts/skills/check_style/scripts/run_rubocop.sh`:
151
+
152
+ - Tool name: `run_rubocop`
153
+ - Description: first `# comment` line in the file, else `"Run run_rubocop"`
154
+ - Input: optional `args` string passed as CLI arguments
155
+ - Execution: `Open3.capture2e("bash #{path} #{args}")`, returns combined output
156
+ - Not executable → log warn, skip (no error raised to the robot)
157
+
158
+ ---
159
+
160
+ ## Similarity Threshold
161
+
162
+ Default: `0.70` (cosine similarity, 0..1 range).
163
+
164
+ Accessible as `RobotLab::AgentSkillMatching::SIMILARITY_THRESHOLD`. Not yet user-configurable in v1; a future `config.agent_skill_threshold` can override it.
165
+
166
+ ---
167
+
168
+ ## Error Handling
169
+
170
+ | Situation | Behavior |
171
+ |---|---|
172
+ | `SKILL.md` missing `name` or `description` | `ConfigurationError` raised at catalog load |
173
+ | fastembed fails on message | Log warn, skip all AgentSkill injection for that call |
174
+ | Script not executable | Log warn, skip that tool; other scripts still wrap |
175
+ | No skills match threshold | Normal `run()` with no injection |
176
+ | Circular skill reference via AgentSkill | Existing cycle detection in `expand_skills` handles it |
177
+
178
+ ---
179
+
180
+ ## Testing
181
+
182
+ ### Unit tests (`test/robot_lab/agent_skill_test.rb`)
183
+ - Parses `SKILL.md` front matter correctly
184
+ - Raises `ConfigurationError` when `name` or `description` missing
185
+ - `instructions` lazy-loads body content
186
+ - `scripts` lazy-globs `scripts/` directory
187
+
188
+ ### Unit tests (`test/robot_lab/agent_skill_catalog_test.rb`)
189
+ - Scans fixture skills directory
190
+ - `find` returns correct `AgentSkill` by symbol
191
+ - `find` returns `nil` for unknown ID
192
+
193
+ ### Unit tests (`test/robot_lab/script_tool_test.rb`)
194
+ - `from_path` produces a tool with correct name and description
195
+ - Tool execution returns stdout+stderr
196
+ - Non-executable script skipped with warning
197
+
198
+ ### Unit tests (`test/robot_lab/robot/agent_skill_matching_test.rb`)
199
+ - `match_agent_skills` returns skills above threshold (mock embeddings)
200
+ - `match_agent_skills` returns empty when below threshold
201
+ - `inject_agent_skills` prepends instructions to system prompt
202
+ - `restore_after_agent_skills` removes injected tools and restores prompt
203
+ - Empty `@pending_agent_skills` short-circuits without embedding
204
+
205
+ ### Integration tests (`test/robot_lab/robot_test.rb`)
206
+ - Robot with `skills: [:pm_template, :agent_skill_fixture]` correctly expands PM template eagerly and AgentSkill at runtime
207
+ - AgentSkill scripts appear in tool list during run, absent after
208
+
209
+ ### Fixtures
210
+ - `test/fixtures/skills/test_skill/SKILL.md` — valid skill
211
+ - `test/fixtures/skills/bad_skill/SKILL.md` — missing description
212
+ - `test/fixtures/skills/scripted_skill/SKILL.md` + `scripts/hello.sh`
213
+
214
+ ---
215
+
216
+ ## File Layout
217
+
218
+ ```
219
+ lib/robot_lab/
220
+ agent_skill.rb # AgentSkill value object
221
+ agent_skill_catalog.rb # Singleton scanner/registry
222
+ script_tool.rb # ScriptTool factory
223
+ robot/
224
+ agent_skill_matching.rb # run() override mixin
225
+
226
+ test/robot_lab/
227
+ agent_skill_test.rb
228
+ agent_skill_catalog_test.rb
229
+ script_tool_test.rb
230
+ robot/
231
+ agent_skill_matching_test.rb
232
+
233
+ test/fixtures/skills/
234
+ test_skill/SKILL.md
235
+ bad_skill/SKILL.md
236
+ scripted_skill/SKILL.md
237
+ scripted_skill/scripts/hello.sh
238
+ ```
239
+
240
+ ---
241
+
242
+ ## Open Questions (deferred)
243
+
244
+ - `references/` and `assets/` subdirectories: make content available as context variables in future
245
+ - User-configurable similarity threshold via `RobotLab.config`
246
+ - Auto-catalog mode: discover all skills without explicit `skills:` listing
247
+ - AgentSkill nesting (a SKILL.md referencing other skill IDs)
data/examples/.envrc ADDED
@@ -0,0 +1 @@
1
+ export ROBOT_LAB_TEMPLATE_PATH="${PWD}/prompts"
@@ -8,20 +8,16 @@
8
8
  # Usage:
9
9
  # ANTHROPIC_API_KEY=your_key ruby examples/01_simple_robot.rb
10
10
 
11
- # Configure template path before loading (MywayConfig reads env vars on init)
12
- ENV['ROBOT_LAB_TEMPLATE_PATH'] ||= File.join(__dir__, "prompts")
13
-
14
- require_relative "../lib/robot_lab"
11
+ require_relative "common"
15
12
 
16
13
  # Create a simple robot using a template
17
14
  robot = RobotLab.build(
15
+ model: LLM[:default].model,
18
16
  name: "helper",
19
- template: :helper,
20
- model: "claude-3-haiku-20240307"
17
+ template: :helper
21
18
  )
22
19
 
23
- puts "Running simple robot..."
24
- puts "-" * 40
20
+ banner "Simple Robot"
25
21
 
26
22
  # Run the robot
27
23
  result = robot.run("What is 2 + 2? Please explain your reasoning briefly.")
@@ -32,4 +28,4 @@ puts "Output:"
32
28
  result.output.each do |message|
33
29
  puts " #{message.content}" if message.respond_to?(:content)
34
30
  end
35
- puts "-" * 40
31
+ hr
data/examples/02_tools.rb CHANGED
@@ -8,10 +8,7 @@
8
8
  # Usage:
9
9
  # ANTHROPIC_API_KEY=your_key ruby examples/02_tools.rb
10
10
 
11
- # Configure template path before loading (MywayConfig reads env vars on init)
12
- ENV['ROBOT_LAB_TEMPLATE_PATH'] ||= File.join(__dir__, "prompts")
13
-
14
- require_relative "../lib/robot_lab"
11
+ require_relative "common"
15
12
 
16
13
  # Define tools using RubyLLM::Tool
17
14
  class Calculator < RubyLLM::Tool
@@ -81,14 +78,13 @@ end
81
78
 
82
79
  # Create robot with tools
83
80
  robot = RobotLab.build(
81
+ model: LLM[:default].model,
84
82
  name: "assistant",
85
83
  template: :assistant,
86
- local_tools: [Calculator, FortuneCookie],
87
- model: "claude-3-haiku-20240307"
84
+ local_tools: [Calculator, FortuneCookie]
88
85
  )
89
86
 
90
- puts "Running robot with tools..."
91
- puts "-" * 40
87
+ banner "Robot with Tools"
92
88
 
93
89
  # Run the robot
94
90
  result = robot.run("What is 15 multiplied by 7? Also, give me a fortune about my career.")
@@ -100,4 +96,4 @@ result.output.each do |message|
100
96
  puts " #{message.content}" if message.respond_to?(:content)
101
97
  end
102
98
 
103
- puts "-" * 40
99
+ hr
@@ -9,10 +9,7 @@
9
9
  # Usage:
10
10
  # ANTHROPIC_API_KEY=your_key ruby examples/03_network.rb
11
11
 
12
- # Configure template path before loading (MywayConfig reads env vars on init)
13
- ENV['ROBOT_LAB_TEMPLATE_PATH'] ||= File.join(__dir__, "prompts")
14
-
15
- require_relative "../lib/robot_lab"
12
+ require_relative "common"
16
13
 
17
14
  # Classifier robot that activates the appropriate specialist
18
15
  class ClassifierRobot < RobotLab::Robot
@@ -40,7 +37,7 @@ class ClassifierRobot < RobotLab::Robot
40
37
  end
41
38
 
42
39
  # Shared RunConfig — all robots in this network use the same model
43
- shared_config = RobotLab::RunConfig.new(model: "claude-3-haiku-20240307")
40
+ shared_config = RobotLab::RunConfig.new(model: LLM[:default].model)
44
41
 
45
42
  # Create specialized robots (no model: needed — inherited from RunConfig)
46
43
  classifier = ClassifierRobot.new(
@@ -50,18 +47,21 @@ classifier = ClassifierRobot.new(
50
47
  )
51
48
 
52
49
  billing_robot = RobotLab.build(
50
+ model: LLM[:default].model,
53
51
  name: "billing",
54
52
  template: :billing,
55
53
  config: shared_config
56
54
  )
57
55
 
58
56
  technical_robot = RobotLab.build(
57
+ model: LLM[:default].model,
59
58
  name: "technical",
60
59
  template: :technical,
61
60
  config: shared_config
62
61
  )
63
62
 
64
63
  general_robot = RobotLab.build(
64
+ model: LLM[:default].model,
65
65
  name: "general",
66
66
  template: :general,
67
67
  config: shared_config
@@ -75,11 +75,10 @@ network = RobotLab.create_network(name: "support_network", config: shared_config
75
75
  task :general, general_robot, depends_on: :optional
76
76
  end
77
77
 
78
- puts "Running multi-robot network..."
79
- puts "-" * 40
78
+ banner "Multi-Robot Network"
80
79
  puts "Network structure:"
81
80
  puts network.visualize
82
- puts "-" * 40
81
+ hr
83
82
 
84
83
  # Run the network with a billing question
85
84
  result = network.run(message: "I was charged twice for my subscription last month. Can you help?")
@@ -102,4 +101,4 @@ if result.value.is_a?(RobotLab::RobotResult)
102
101
  puts " Response: #{content[0..200]}..." if content
103
102
  end
104
103
 
105
- puts "-" * 40
104
+ hr
data/examples/04_mcp.rb CHANGED
@@ -21,10 +21,7 @@
21
21
  # - Reading file contents and repository information
22
22
  # - Managing branches and commits
23
23
 
24
- # Configure template path before loading (MywayConfig reads env vars on init)
25
- ENV['ROBOT_LAB_TEMPLATE_PATH'] ||= File.join(__dir__, "prompts")
26
-
27
- require_relative "../lib/robot_lab"
24
+ require_relative "common"
28
25
 
29
26
  # GitHub MCP server configuration using StdIO transport
30
27
  github_server = {
@@ -39,14 +36,11 @@ github_server = {
39
36
  }
40
37
  }
41
38
 
42
- puts <<~HEADER
43
- MCP Integration Example: GitHub
44
- #{"=" * 40}
45
-
46
- This example demonstrates using the GitHub MCP server to interact
47
- with GitHub repositories through the Model Context Protocol.
39
+ banner "MCP Integration Example: GitHub"
48
40
 
49
- HEADER
41
+ puts "This example demonstrates using the GitHub MCP server to interact"
42
+ puts "with GitHub repositories through the Model Context Protocol."
43
+ puts
50
44
 
51
45
  # Verify prerequisites
52
46
  unless ENV["GITHUB_PERSONAL_ACCESS_TOKEN"]
@@ -65,9 +59,7 @@ end
65
59
  # Part 1: Direct MCP Client Usage
66
60
  # ============================================================================
67
61
 
68
- puts "PART 1: Direct MCP Client Usage"
69
- puts "-" * 40
70
- puts
62
+ section "Part 1: Direct MCP Client Usage"
71
63
  puts "Connecting to GitHub MCP server..."
72
64
 
73
65
  begin
@@ -91,7 +83,7 @@ begin
91
83
 
92
84
  # List available tools
93
85
  puts "Available GitHub Tools:"
94
- puts "-" * 40
86
+ hr
95
87
 
96
88
  tools = client.list_tools
97
89
  if tools.empty?
@@ -103,7 +95,7 @@ begin
103
95
  end
104
96
  end
105
97
 
106
- puts "-" * 40
98
+ hr
107
99
  puts "Total: #{tools.size} tools available"
108
100
  puts
109
101
 
@@ -134,22 +126,22 @@ begin
134
126
  # Part 2: Robot + MCP Integration
135
127
  # ============================================================================
136
128
 
137
- puts
138
- puts "=" * 40
139
- puts "PART 2: Robot + MCP Integration"
140
- puts "-" * 40
141
- puts
129
+
130
+ section "Part 2: Robot + MCP Integration"
142
131
 
143
132
  puts "Creating Robot with MCP server integration..."
144
133
  puts
145
134
 
146
- # Create a Robot with MCP server - tools are automatically discovered
135
+ # Create a Robot with MCP server - tools are automatically discovered.
136
+ # connect_mcp! forces eager connection; without it MCP clients initialize
137
+ # lazily on the first run() call, leaving mcp_clients empty until then.
147
138
  robot = RobotLab.build(
139
+ model: LLM[:default].model,
148
140
  name: "github_assistant",
149
141
  template: :github_assistant,
150
- mcp_servers: [github_server],
151
- model: "claude-3-haiku-20240307"
142
+ mcp_servers: [github_server]
152
143
  )
144
+ robot.connect_mcp!
153
145
 
154
146
  puts "Robot created: #{robot.name}"
155
147
  puts " Model: #{robot.model}"
@@ -159,7 +151,7 @@ begin
159
151
 
160
152
  # Show discovered MCP tools
161
153
  puts "Discovered MCP Tools:"
162
- puts "-" * 40
154
+ hr
163
155
  robot.mcp_tools.first(10).each do |tool|
164
156
  puts " #{tool.name}"
165
157
  puts " #{tool.description&.slice(0, 60)}..." if tool.description
@@ -170,13 +162,13 @@ begin
170
162
  # Run the robot with a query that will use MCP tools
171
163
  puts "Running Robot with a GitHub query..."
172
164
  puts "Query: 'What are the top 3 most starred Ruby web frameworks on GitHub?'"
173
- puts "-" * 40
165
+ hr
174
166
 
175
167
  result = robot.run("What are the top 3 most starred Ruby web frameworks on GitHub? Just list their names and star counts.")
176
168
 
177
169
  puts
178
170
  puts "Robot Response:"
179
- puts "-" * 40
171
+ hr
180
172
  result.output.each do |msg|
181
173
  puts msg.content if msg.respond_to?(:content)
182
174
  end
@@ -185,7 +177,7 @@ begin
185
177
  # Show tool calls if any were made
186
178
  if result.tool_calls.any?
187
179
  puts "Tool Calls Made:"
188
- puts "-" * 40
180
+ hr
189
181
  result.tool_calls.each do |tc|
190
182
  tool_info = tc.respond_to?(:tool) ? tc.tool : tc
191
183
  puts " #{tool_info[:name] || tool_info}"
@@ -212,5 +204,5 @@ rescue Errno::ENOENT
212
204
  end
213
205
 
214
206
  puts
215
- puts "=" * 40
207
+ hr
216
208
  puts "Example complete!"
@@ -11,10 +11,7 @@
11
11
  # Usage:
12
12
  # ANTHROPIC_API_KEY=your_key ruby examples/05_streaming.rb
13
13
 
14
- # Configure template path before loading (MywayConfig reads env vars on init)
15
- ENV['ROBOT_LAB_TEMPLATE_PATH'] ||= File.join(__dir__, "prompts")
16
-
17
- require_relative "../lib/robot_lab"
14
+ require_relative "common"
18
15
 
19
16
  # Send logger output to a file instead of stdout
20
17
  require 'logger'
@@ -22,19 +19,17 @@ log_file = File.join(__dir__, "05.log")
22
19
  RobotLab.config.logger = Logger.new(log_file)
23
20
  RubyLLM.configure { |c| c.logger = Logger.new(log_file) }
24
21
 
25
- puts "Streaming Content Example"
26
- puts "=" * 50
27
- puts ""
22
+ banner "Streaming Content"
28
23
 
29
24
  # ── 1. Stored callback (on_content:) ─────────────────────────────
30
25
  #
31
26
  # Wire streaming at build time. The callback fires on every run() call.
32
27
 
33
- puts "1. Stored callback (on_content:)"
34
- puts "-" * 50
28
+ section "1. Stored Callback (on_content:)"
35
29
 
36
30
  chunks_received = 0
37
31
  robot = RobotLab.build(
32
+ model: LLM[:default].model,
38
33
  name: "storyteller",
39
34
  system_prompt: "You are a concise storyteller. Keep responses under 3 sentences.",
40
35
  on_content: ->(chunk) {
@@ -53,11 +48,11 @@ puts ""
53
48
  #
54
49
  # Pass a block to run() for one-off streaming.
55
50
 
56
- puts "2. Per-call block"
57
- puts "-" * 50
51
+ section "2. Per-call Block"
58
52
 
59
53
  block_chunks = 0
60
54
  bare_robot = RobotLab.build(
55
+ model: LLM[:default].model,
61
56
  name: "factbot",
62
57
  system_prompt: "You are concise. Answer in one sentence."
63
58
  )
@@ -75,13 +70,13 @@ puts ""
75
70
  #
76
71
  # When both exist, both fire: stored callback first, then block.
77
72
 
78
- puts "3. Both together (stored fires first, then block)"
79
- puts "-" * 50
73
+ section "3. Both Together (stored fires first, then block)"
80
74
 
81
75
  stored_log = []
82
76
  block_log = []
83
77
 
84
78
  combo_robot = RobotLab.build(
79
+ model: LLM[:default].model,
85
80
  name: "combo",
86
81
  system_prompt: "You are concise. Answer in one sentence.",
87
82
  on_content: ->(chunk) { stored_log << chunk.content }
@@ -101,11 +96,11 @@ puts ""
101
96
  #
102
97
  # on_content participates in the config cascade.
103
98
 
104
- puts "4. Via RunConfig (config cascade)"
105
- puts "-" * 50
99
+ section "4. Via RunConfig (config cascade)"
106
100
 
107
101
  config_chunks = 0
108
102
  config = RobotLab::RunConfig.new(
103
+ model: LLM[:default].model,
109
104
  on_content: ->(chunk) {
110
105
  print chunk.content
111
106
  config_chunks += 1
@@ -113,6 +108,7 @@ config = RobotLab::RunConfig.new(
113
108
  )
114
109
 
115
110
  config_robot = RobotLab.build(
111
+ model: LLM[:default].model,
116
112
  name: "config_bot",
117
113
  system_prompt: "You are concise. Answer in one sentence.",
118
114
  config: config
@@ -126,9 +122,7 @@ puts ""
126
122
 
127
123
  # ── Summary ──────────────────────────────────────────────────────
128
124
 
129
- puts "=" * 50
130
- puts "Summary"
131
- puts ""
125
+ section "Summary"
132
126
  puts <<~SUMMARY
133
127
  on_content: callback — wired at build time, fires every run()
134
128
  run() { |chunk| ... } — per-call streaming block