robot_lab 0.2.1 → 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/.envrc +1 -0
- data/.loki +60 -0
- data/.quality/reek_baseline.txt +43 -0
- data/.rubocop.yml +5 -167
- data/CHANGELOG.md +54 -0
- data/README.md +185 -64
- data/Rakefile +28 -12
- data/docs/api/core/index.md +41 -14
- data/docs/api/core/memory.md +247 -29
- data/docs/api/core/network.md +285 -33
- data/docs/api/core/result.md +120 -32
- data/docs/api/core/robot.md +636 -60
- data/docs/api/core/state.md +87 -197
- data/docs/api/core/tool.md +165 -20
- data/docs/api/errors.md +152 -14
- data/docs/api/hooks.md +469 -0
- data/docs/api/index.md +83 -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 +118 -30
- data/docs/architecture/index.md +86 -11
- data/docs/architecture/message-flow.md +66 -29
- data/docs/architecture/network-orchestration.md +153 -38
- data/docs/architecture/robot-execution.md +173 -91
- 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 +299 -93
- data/docs/getting-started/installation.md +53 -41
- data/docs/getting-started/quick-start.md +51 -6
- data/docs/guides/building-robots.md +296 -50
- data/docs/guides/creating-networks.md +220 -31
- data/docs/guides/hooks.md +1084 -0
- data/docs/guides/index.md +5 -0
- data/docs/guides/knowledge.md +37 -6
- data/docs/guides/mcp-integration.md +211 -44
- data/docs/guides/memory.md +103 -12
- data/docs/guides/observability.md +166 -51
- data/docs/guides/streaming.md +184 -125
- data/docs/guides/using-tools.md +300 -11
- data/docs/index.md +37 -5
- data/examples/01_simple_robot.rb +2 -2
- data/examples/02_tools.rb +14 -4
- data/examples/03_network.rb +23 -9
- 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 +266 -0
- data/examples/README.md +203 -42
- data/examples/common.rb +79 -7
- data/examples/xyzzy.rb +97 -0
- data/lib/robot_lab/agent_skill.rb +5 -4
- data/lib/robot_lab/budget/ledger.rb +98 -0
- data/lib/robot_lab/capabilities.rb +84 -0
- data/lib/robot_lab/config/defaults.yml +10 -0
- data/lib/robot_lab/config.rb +14 -9
- data/lib/robot_lab/error.rb +34 -4
- data/lib/robot_lab/errors.rb +45 -0
- data/lib/robot_lab/hook.rb +79 -0
- data/lib/robot_lab/hook_context.rb +194 -0
- data/lib/robot_lab/hook_registry.rb +55 -0
- data/lib/robot_lab/hooks.rb +87 -0
- data/lib/robot_lab/mcp/connection_poller.rb +2 -2
- data/lib/robot_lab/names.rb +402 -0
- data/lib/robot_lab/narrator.rb +87 -0
- data/lib/robot_lab/network.rb +77 -20
- data/lib/robot_lab/robot/agent_skill_matching.rb +1 -3
- data/lib/robot_lab/robot/budget.rb +89 -0
- data/lib/robot_lab/robot/bus_messaging.rb +90 -18
- data/lib/robot_lab/robot/hooking.rb +56 -0
- data/lib/robot_lab/robot/mcp_management.rb +8 -2
- data/lib/robot_lab/robot/template_rendering.rb +18 -5
- data/lib/robot_lab/robot.rb +233 -89
- data/lib/robot_lab/run_config.rb +24 -5
- data/lib/robot_lab/runnable.rb +51 -0
- data/lib/robot_lab/sandbox/null.rb +13 -0
- data/lib/robot_lab/sandbox/seatbelt.rb +104 -0
- data/lib/robot_lab/sandbox.rb +52 -0
- data/lib/robot_lab/script_tool.rb +60 -2
- data/lib/robot_lab/task.rb +26 -20
- data/lib/robot_lab/tool.rb +52 -11
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +70 -15
- data/mkdocs.yml +7 -1
- metadata +41 -99
- data/examples/temp.md +0 -51
- data/site/404.html +0 -2300
- data/site/api/core/index.html +0 -2706
- data/site/api/core/memory/index.html +0 -3793
- data/site/api/core/network/index.html +0 -3500
- data/site/api/core/robot/index.html +0 -4566
- data/site/api/core/state/index.html +0 -3390
- data/site/api/core/tool/index.html +0 -3843
- data/site/api/index.html +0 -2635
- data/site/api/mcp/client/index.html +0 -3435
- data/site/api/mcp/index.html +0 -2783
- data/site/api/mcp/server/index.html +0 -3252
- data/site/api/mcp/transports/index.html +0 -3352
- data/site/api/messages/index.html +0 -2641
- data/site/api/messages/text-message/index.html +0 -3087
- data/site/api/messages/tool-call-message/index.html +0 -3159
- data/site/api/messages/tool-result-message/index.html +0 -3252
- data/site/api/messages/user-message/index.html +0 -3212
- data/site/api/streaming/context/index.html +0 -3282
- data/site/api/streaming/events/index.html +0 -3347
- data/site/api/streaming/index.html +0 -2738
- data/site/architecture/core-concepts/index.html +0 -3757
- data/site/architecture/index.html +0 -2797
- data/site/architecture/message-flow/index.html +0 -3238
- data/site/architecture/network-orchestration/index.html +0 -3433
- data/site/architecture/robot-execution/index.html +0 -3140
- data/site/architecture/state-management/index.html +0 -3498
- data/site/assets/css/custom.css +0 -56
- data/site/assets/images/favicon.png +0 -0
- data/site/assets/images/robot_lab.jpg +0 -0
- data/site/assets/javascripts/bundle.79ae519e.min.js +0 -16
- data/site/assets/javascripts/bundle.79ae519e.min.js.map +0 -7
- data/site/assets/javascripts/lunr/min/lunr.ar.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.da.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.de.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.du.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.el.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.es.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.fi.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.fr.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.he.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.hi.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.hu.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.hy.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.it.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ja.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.jp.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.kn.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.ko.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.multi.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.nl.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.no.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.pt.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ro.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ru.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.sa.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.sv.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ta.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.te.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.th.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.tr.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.vi.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.zh.min.js +0 -1
- data/site/assets/javascripts/lunr/tinyseg.js +0 -206
- data/site/assets/javascripts/lunr/wordcut.js +0 -6708
- data/site/assets/javascripts/workers/search.2c215733.min.js +0 -42
- data/site/assets/javascripts/workers/search.2c215733.min.js.map +0 -7
- data/site/assets/stylesheets/main.484c7ddc.min.css +0 -1
- data/site/assets/stylesheets/main.484c7ddc.min.css.map +0 -1
- data/site/assets/stylesheets/palette.ab4e12ef.min.css +0 -1
- data/site/assets/stylesheets/palette.ab4e12ef.min.css.map +0 -1
- data/site/concepts/index.html +0 -3455
- data/site/examples/basic-chat/index.html +0 -2880
- data/site/examples/index.html +0 -2907
- data/site/examples/mcp-server/index.html +0 -3018
- data/site/examples/multi-robot-network/index.html +0 -3131
- data/site/examples/rails-application/index.html +0 -3329
- data/site/examples/tool-usage/index.html +0 -3085
- data/site/getting-started/configuration/index.html +0 -3745
- data/site/getting-started/index.html +0 -2572
- data/site/getting-started/installation/index.html +0 -2981
- data/site/getting-started/quick-start/index.html +0 -2942
- data/site/guides/building-robots/index.html +0 -4290
- data/site/guides/creating-networks/index.html +0 -3858
- data/site/guides/index.html +0 -2586
- data/site/guides/mcp-integration/index.html +0 -3581
- data/site/guides/memory/index.html +0 -3586
- data/site/guides/rails-integration/index.html +0 -4019
- data/site/guides/streaming/index.html +0 -3157
- data/site/guides/using-tools/index.html +0 -3802
- data/site/index.html +0 -2671
- data/site/search/search_index.json +0 -1
- data/site/sitemap.xml +0 -183
- data/site/sitemap.xml.gz +0 -0
- data/site/tags.json +0 -1
data/docs/guides/index.md
CHANGED
|
@@ -42,6 +42,10 @@ If you're new to RobotLab, start here:
|
|
|
42
42
|
|
|
43
43
|
Token tracking, circuit breakers, doom loop detection, auto compaction, and learning accumulation
|
|
44
44
|
|
|
45
|
+
- [:octicons-zap-24: **Hook System**](hooks.md)
|
|
46
|
+
|
|
47
|
+
Lifecycle hooks for instrumentation, response caching, extensions, and cross-cutting concerns
|
|
48
|
+
|
|
45
49
|
- [:octicons-search-24: **Knowledge & Retrieval**](knowledge.md)
|
|
46
50
|
|
|
47
51
|
Chat history search and embedding-based document store for RAG workflows
|
|
@@ -59,6 +63,7 @@ If you're new to RobotLab, start here:
|
|
|
59
63
|
| [Streaming](streaming.md) | Real-time responses | 5 min |
|
|
60
64
|
| [Memory](memory.md) | Shared data store | 5 min |
|
|
61
65
|
| [Observability & Safety](observability.md) | Token tracking, circuit breaker, doom loop detection, auto compaction, learning loop | 10 min |
|
|
66
|
+
| [Hook System](hooks.md) | Lifecycle hooks, extensions, response caching, instrumentation | 15 min |
|
|
62
67
|
| [Knowledge & Retrieval](knowledge.md) | Chat history search and embedding-based document store (RAG) | 10 min |
|
|
63
68
|
|
|
64
69
|
## Extension Gems
|
data/docs/guides/knowledge.md
CHANGED
|
@@ -21,6 +21,18 @@ results = robot.search_history(query, limit: 5)
|
|
|
21
21
|
|
|
22
22
|
Scores every message in the robot's conversation history against `query` using stemmed term-frequency cosine similarity (via the `classifier` gem). Returns up to `limit` `HistoryResult` objects sorted by score descending.
|
|
23
23
|
|
|
24
|
+
> [!NOTE]
|
|
25
|
+
> **Every** message in `@chat.messages` is scored — there is no role filter.
|
|
26
|
+
> Results routinely include the `:system` prompt and raw `:tool` result payloads
|
|
27
|
+
> (often JSON) alongside `:user` and `:assistant` turns. Filter by role yourself
|
|
28
|
+
> if you only want the conversation:
|
|
29
|
+
>
|
|
30
|
+
> ```ruby
|
|
31
|
+
> hits = robot.search_history(query, limit: 20)
|
|
32
|
+
> .select { |r| %i[user assistant].include?(r.role) }
|
|
33
|
+
> .first(3)
|
|
34
|
+
> ```
|
|
35
|
+
|
|
24
36
|
```ruby
|
|
25
37
|
results = robot.search_history("quarterly revenue", limit: 3)
|
|
26
38
|
|
|
@@ -35,7 +47,7 @@ end
|
|
|
35
47
|
| Field | Type | Description |
|
|
36
48
|
|-------|------|-------------|
|
|
37
49
|
| `text` | String | The message text |
|
|
38
|
-
| `role` | Symbol | `:user`, `:assistant`, or `:
|
|
50
|
+
| `role` | Symbol | The message's role — `:user`, `:assistant`, `:system`, or `:tool` |
|
|
39
51
|
| `score` | Float (0.0–1.0) | Cosine similarity with the query |
|
|
40
52
|
| `index` | Integer | Position in `@chat.messages` |
|
|
41
53
|
|
|
@@ -89,7 +101,7 @@ Without it, calling `search_history` raises `RobotLab::DependencyError` with an
|
|
|
89
101
|
|
|
90
102
|
### The Problem
|
|
91
103
|
|
|
92
|
-
Sometimes the knowledge you need isn't in the conversation history — it's in a README, a product spec, a changelog. `store_document` / `search_documents` embed arbitrary text
|
|
104
|
+
Sometimes the knowledge you need isn't in the conversation history — it's in a README, a product spec, a changelog. `store_document` / `search_documents` embed arbitrary text and retrieve the most relevant chunk at query time. Embeddings come from `fastembed` when it is installed, and from a built-in TF-IDF fallback when it is not (see [Embedding Model](#embedding-model)).
|
|
93
105
|
|
|
94
106
|
### memory.store_document / memory.search_documents
|
|
95
107
|
|
|
@@ -136,7 +148,7 @@ store.clear
|
|
|
136
148
|
|
|
137
149
|
Default: `BAAI/bge-small-en-v1.5` (~23 MB, downloaded on first use, cached in `~/.cache/fastembed/`).
|
|
138
150
|
|
|
139
|
-
Documents are embedded
|
|
151
|
+
Documents and queries are embedded asymmetrically: stored text goes through `Fastembed::TextEmbedding#passage_embed`, query text through `#query_embed`. RobotLab does **not** prepend any literal `"passage: "` / `"query: "` string of its own — it hands the raw text to the two methods and lets fastembed handle whatever instruction prefixing the model expects.
|
|
140
152
|
|
|
141
153
|
Custom model:
|
|
142
154
|
|
|
@@ -144,6 +156,23 @@ Custom model:
|
|
|
144
156
|
store = RobotLab::DocumentStore.new(model_name: "BAAI/bge-base-en-v1.5")
|
|
145
157
|
```
|
|
146
158
|
|
|
159
|
+
### TF-IDF Fallback
|
|
160
|
+
|
|
161
|
+
`DocumentStore` decides once, at load time, whether `fastembed` is available
|
|
162
|
+
(`RobotLab::DocumentStore::FASTEMBED_AVAILABLE`). When it is **not**, the store
|
|
163
|
+
still works — it silently falls back to a stemmed, stop-word-filtered,
|
|
164
|
+
L2-normalised term-frequency embedder and compares those sparse vectors by
|
|
165
|
+
cosine similarity. Nothing is downloaded and everything runs offline, which
|
|
166
|
+
makes it convenient for development and CI.
|
|
167
|
+
|
|
168
|
+
> [!WARNING]
|
|
169
|
+
> The fallback is **lexical, not semantic**. It matches shared word stems only,
|
|
170
|
+
> so a query and a document that mean the same thing in different words score
|
|
171
|
+
> `0.0`. There is no error, no warning, and no change to the API — only
|
|
172
|
+
> noticeably worse results. Check `RobotLab::DocumentStore::FASTEMBED_AVAILABLE`
|
|
173
|
+
> if you need to know which path you are on, and note that `model_name:` is
|
|
174
|
+
> ignored entirely when it is `false`.
|
|
175
|
+
|
|
147
176
|
### RAG Pattern
|
|
148
177
|
|
|
149
178
|
```ruby
|
|
@@ -177,12 +206,14 @@ The embedding-based document store requires the [`robot_lab-document_store`](htt
|
|
|
177
206
|
gem "robot_lab-document_store"
|
|
178
207
|
```
|
|
179
208
|
|
|
180
|
-
|
|
209
|
+
The gem uses `fastembed` for ONNX-based embeddings when it is installed: the `BAAI/bge-small-en-v1.5` model (~23 MB) is downloaded on first use and cached in `~/.cache/fastembed/`. If `fastembed` cannot be loaded, `DocumentStore` still works via the lexical [TF-IDF fallback](#tf-idf-fallback).
|
|
210
|
+
|
|
211
|
+
Without `robot_lab-document_store` itself loaded, calling `memory.store_document`, `memory.search_documents`, `memory.document_keys`, or `memory.delete_document` raises `RobotLab::DependencyError`.
|
|
181
212
|
|
|
182
213
|
---
|
|
183
214
|
|
|
184
215
|
## See Also
|
|
185
216
|
|
|
186
217
|
- [Observability Guide](observability.md)
|
|
187
|
-
- [
|
|
188
|
-
- [
|
|
218
|
+
- [examples/25_history_search.rb](https://github.com/MadBomber/robot_lab/blob/main/examples/25_history_search.rb) — Chat History Search
|
|
219
|
+
- [examples/26_document_store.rb](https://github.com/MadBomber/robot_lab/blob/main/examples/26_document_store.rb) — Embedding-Based Document Store
|
|
@@ -50,9 +50,10 @@ MCP servers can be declared directly in a template's YAML front matter, making t
|
|
|
50
50
|
description: GitHub assistant with MCP tool access
|
|
51
51
|
mcp:
|
|
52
52
|
- name: github
|
|
53
|
-
transport:
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
transport:
|
|
54
|
+
type: stdio
|
|
55
|
+
command: npx
|
|
56
|
+
args: ["-y", "@modelcontextprotocol/server-github"]
|
|
56
57
|
---
|
|
57
58
|
You are a helpful GitHub assistant with access to GitHub tools via MCP.
|
|
58
59
|
```
|
|
@@ -64,6 +65,16 @@ robot = RobotLab.build(template: :github_assistant)
|
|
|
64
65
|
|
|
65
66
|
Constructor `mcp:` overrides frontmatter `mcp:` when provided.
|
|
66
67
|
|
|
68
|
+
> [!CAUTION]
|
|
69
|
+
> `transport:` must be a **nested mapping**, exactly as above. The flat form —
|
|
70
|
+
> `transport: stdio` with sibling `command:`/`args:` keys — does not raise:
|
|
71
|
+
> `Server` calls `transform_keys` on the transport value, a String raises
|
|
72
|
+
> `NoMethodError: undefined method 'transform_keys' for an instance of String`,
|
|
73
|
+
> and that exception is caught by MCP setup's rescue. The robot builds and runs
|
|
74
|
+
> with **zero tools** from that server. It is not silent, though: the failure is
|
|
75
|
+
> logged at `WARN` through `RobotLab.config.logger` (`$stdout` by default) and
|
|
76
|
+
> the server name is recorded in `robot.failed_mcp_server_names`.
|
|
77
|
+
|
|
67
78
|
### Hierarchical Configuration
|
|
68
79
|
|
|
69
80
|
The `mcp:` parameter supports three modes:
|
|
@@ -96,14 +107,21 @@ MCP configuration resolves through a hierarchy: **runtime > robot build > networ
|
|
|
96
107
|
|
|
97
108
|
```
|
|
98
109
|
Global (RobotLab.config.mcp)
|
|
99
|
-
-> Network (task
|
|
110
|
+
-> Network (network config: / task config:)
|
|
100
111
|
-> Robot (mcp: :inherit | :none | [...])
|
|
101
|
-
-> Runtime (robot.run("msg", mcp: [...]))
|
|
112
|
+
-> Runtime (robot.run("msg", mcp: [...]), and task mcp: [...])
|
|
102
113
|
```
|
|
103
114
|
|
|
115
|
+
> [!NOTE]
|
|
116
|
+
> A task's `mcp:`/`tools:` are **not** a separate network tier — `Task` puts
|
|
117
|
+
> them into the run params, so they arrive as the **runtime** value for that
|
|
118
|
+
> robot's `run()`. Only a `config:` (on the network or on the task) acts as the
|
|
119
|
+
> parent level that `:inherit` resolves against.
|
|
120
|
+
|
|
104
121
|
## Timeout Configuration
|
|
105
122
|
|
|
106
|
-
|
|
123
|
+
Every server config accepts a `timeout:`. The default is 15 seconds
|
|
124
|
+
(`RobotLab::MCP::Server::DEFAULT_TIMEOUT`). Set it at the server level:
|
|
107
125
|
|
|
108
126
|
```ruby
|
|
109
127
|
robot = RobotLab.build(
|
|
@@ -119,10 +137,46 @@ robot = RobotLab.build(
|
|
|
119
137
|
)
|
|
120
138
|
```
|
|
121
139
|
|
|
122
|
-
Values >= 1000 are auto-converted from milliseconds to seconds. The minimum timeout is 1 second
|
|
140
|
+
Values >= 1000 are auto-converted from milliseconds to seconds. The minimum timeout is 1 second:
|
|
141
|
+
|
|
142
|
+
| Given | Stored |
|
|
143
|
+
|-------|--------|
|
|
144
|
+
| `nil` | `15` (the default) |
|
|
145
|
+
| `30` | `30.0` |
|
|
146
|
+
| `5000` | `5.0` (read as milliseconds) |
|
|
147
|
+
| `0.5` | `1` (floored to the 1-second minimum) |
|
|
148
|
+
|
|
149
|
+
> [!WARNING]
|
|
150
|
+
> **Only the Stdio transport actually enforces the timeout.** SSE, WebSocket,
|
|
151
|
+
> and StreamableHTTP store the value and never reference it — a remote MCP
|
|
152
|
+
> server that stops responding will hang the call indefinitely regardless of
|
|
153
|
+
> what you set here. Apply your own timeout around remote-transport calls if
|
|
154
|
+
> you need one.
|
|
123
155
|
|
|
124
156
|
## Transport Types
|
|
125
157
|
|
|
158
|
+
Valid `type:` values are exactly: `stdio`, `sse`, `ws`, `websocket`,
|
|
159
|
+
`streamable-http`, `http` (`RobotLab::MCP::Server::VALID_TRANSPORT_TYPES`).
|
|
160
|
+
|
|
161
|
+
An invalid type raises `ArgumentError: Invalid transport type: <type>. Must be
|
|
162
|
+
one of: stdio, sse, ws, websocket, streamable-http, http` — but **only if you
|
|
163
|
+
construct the server or client yourself**. Through the robot (`mcp:` on
|
|
164
|
+
`RobotLab.build`) the error is caught by the same rescue that handles any other
|
|
165
|
+
connect failure: it is logged at `WARN` and the server name is recorded in
|
|
166
|
+
`robot.failed_mcp_server_names`, and the robot carries on with zero tools from
|
|
167
|
+
that server. See [Connection Errors](#connection-errors).
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
bad = { name: "oops", transport: { type: "streamable_http", url: "https://x" } }
|
|
171
|
+
|
|
172
|
+
RobotLab::MCP::Server.new(**bad) # => raises ArgumentError
|
|
173
|
+
|
|
174
|
+
robot = RobotLab.build(name: "r", system_prompt: "...", mcp: [bad])
|
|
175
|
+
robot.run("hi", mcp: :inherit, tools: :inherit)
|
|
176
|
+
# logs: WARN Robot 'r' error connecting to MCP server 'oops': Invalid transport type: ...
|
|
177
|
+
robot.failed_mcp_server_names # => ["oops"]
|
|
178
|
+
```
|
|
179
|
+
|
|
126
180
|
### Stdio Transport
|
|
127
181
|
|
|
128
182
|
Communicate via stdin/stdout with a subprocess:
|
|
@@ -154,7 +208,9 @@ Connect via WebSocket:
|
|
|
154
208
|
```
|
|
155
209
|
|
|
156
210
|
!!! note "Dependency Required"
|
|
157
|
-
WebSocket transport requires the `async-websocket` gem.
|
|
211
|
+
WebSocket transport requires the `async-websocket` gem. (It also reaches for
|
|
212
|
+
`Async::HTTP::Endpoint` without requiring it, so `async-http` must be loaded
|
|
213
|
+
as well.)
|
|
158
214
|
|
|
159
215
|
### SSE Transport
|
|
160
216
|
|
|
@@ -170,6 +226,9 @@ Server-Sent Events transport:
|
|
|
170
226
|
}
|
|
171
227
|
```
|
|
172
228
|
|
|
229
|
+
!!! note "Dependency Required"
|
|
230
|
+
SSE transport requires the `async-http` gem.
|
|
231
|
+
|
|
173
232
|
### HTTP Transport
|
|
174
233
|
|
|
175
234
|
Streamable HTTP transport with session support:
|
|
@@ -178,7 +237,7 @@ Streamable HTTP transport with session support:
|
|
|
178
237
|
{
|
|
179
238
|
name: "http_server",
|
|
180
239
|
transport: {
|
|
181
|
-
type: "streamable_http"
|
|
240
|
+
type: "streamable-http", # or "http" — NOT "streamable_http"
|
|
182
241
|
url: "https://api.example.com/mcp",
|
|
183
242
|
session_id: "optional_session_id",
|
|
184
243
|
auth_provider: -> { "Bearer #{fetch_token}" }
|
|
@@ -186,30 +245,60 @@ Streamable HTTP transport with session support:
|
|
|
186
245
|
}
|
|
187
246
|
```
|
|
188
247
|
|
|
248
|
+
!!! note "Dependency Required"
|
|
249
|
+
Streamable HTTP transport requires the `async-http` gem.
|
|
250
|
+
|
|
251
|
+
> [!CAUTION]
|
|
252
|
+
> The type is spelled with a **hyphen**. `type: "streamable_http"` (underscore)
|
|
253
|
+
> raises `ArgumentError: Invalid transport type: streamable_http`.
|
|
254
|
+
|
|
255
|
+
> [!WARNING]
|
|
256
|
+
> The remote transports connect inside an un-awaited `Async` block and set
|
|
257
|
+
> their connected flag **before** the MCP initialize handshake. `client.connected?`
|
|
258
|
+
> therefore returns `true` even when the host is unreachable — do not treat it
|
|
259
|
+
> as proof the server answered. Stdio is the only transport that blocks on the
|
|
260
|
+
> handshake and reports a connect failure synchronously.
|
|
261
|
+
|
|
189
262
|
## Using MCP Tools
|
|
190
263
|
|
|
191
|
-
Once configured, MCP tools are
|
|
264
|
+
Once configured, MCP tools are discovered on connect and made available to the
|
|
265
|
+
robot. **Connecting is not automatic** — see the warning below:
|
|
192
266
|
|
|
193
267
|
```ruby
|
|
194
268
|
robot = RobotLab.build(
|
|
195
269
|
name: "helper",
|
|
196
|
-
system_prompt: <<~PROMPT
|
|
270
|
+
system_prompt: <<~PROMPT,
|
|
197
271
|
You can help users with GitHub tasks.
|
|
198
272
|
Use available tools to search repositories, create issues, etc.
|
|
199
|
-
PROMPT
|
|
273
|
+
PROMPT
|
|
200
274
|
mcp: [
|
|
201
275
|
{ name: "github", transport: { type: "stdio", command: "mcp-server-github" } }
|
|
202
276
|
]
|
|
203
277
|
)
|
|
204
278
|
|
|
205
|
-
#
|
|
206
|
-
result = robot.run("Find repositories about machine learning")
|
|
279
|
+
# mcp: :inherit connects the servers; tools: :inherit sends their tools to the LLM.
|
|
280
|
+
result = robot.run("Find repositories about machine learning", mcp: :inherit, tools: :inherit)
|
|
207
281
|
puts result.last_text_content
|
|
208
282
|
```
|
|
209
283
|
|
|
284
|
+
> [!CAUTION]
|
|
285
|
+
> **A plain `robot.run(message)` neither connects MCP servers nor sends any
|
|
286
|
+
> tools.** Both the `mcp:` and `tools:` parameters of `run` default to `:none`,
|
|
287
|
+
> which means "zero this turn" — the build-time `mcp:` list is simply not
|
|
288
|
+
> consulted. You need **both** flags:
|
|
289
|
+
>
|
|
290
|
+
> ```ruby
|
|
291
|
+
> robot.run("...") # no MCP connection, no tools
|
|
292
|
+
> robot.run("...", mcp: :inherit) # connects, but sends zero tools
|
|
293
|
+
> robot.run("...", mcp: :inherit, tools: :inherit) # connects AND sends its tools
|
|
294
|
+
> ```
|
|
295
|
+
|
|
210
296
|
## Filtering MCP Tools
|
|
211
297
|
|
|
212
|
-
Use the `tools:` parameter to restrict which tools (including MCP-discovered
|
|
298
|
+
Use the `tools:` parameter to restrict which tools (including MCP-discovered
|
|
299
|
+
tools) reach the LLM. It is a **name allowlist**, and it must be supplied at the
|
|
300
|
+
level that actually runs — the `run()` call (or the network `task`, which
|
|
301
|
+
forwards to `run()`):
|
|
213
302
|
|
|
214
303
|
```ruby
|
|
215
304
|
robot = RobotLab.build(
|
|
@@ -217,14 +306,32 @@ robot = RobotLab.build(
|
|
|
217
306
|
system_prompt: "You help read and search files.",
|
|
218
307
|
mcp: [
|
|
219
308
|
{ name: "filesystem", transport: { type: "stdio", command: "mcp-server-fs" } }
|
|
220
|
-
]
|
|
221
|
-
tools: %w[read_file search_files list_directory] # Only allow specific tools
|
|
309
|
+
]
|
|
222
310
|
)
|
|
311
|
+
|
|
312
|
+
robot.run("Summarise the README",
|
|
313
|
+
mcp: :inherit,
|
|
314
|
+
tools: %w[read_file search_files list_directory]) # only these
|
|
223
315
|
```
|
|
224
316
|
|
|
317
|
+
> [!WARNING]
|
|
318
|
+
> A build-time `tools: %w[...]` allowlist has no effect on its own, because the
|
|
319
|
+
> runtime default of `:none` sends zero tools regardless. And for a
|
|
320
|
+
> **standalone** robot, do not write `tools: :inherit` at build time as a
|
|
321
|
+
> workaround: there the parent level is the global `:none`, so it produces an
|
|
322
|
+
> allowlist of `["none"]` that matches nothing. Leave build-time `tools:` unset
|
|
323
|
+
> and pass the filter at run time.
|
|
324
|
+
>
|
|
325
|
+
> Inside a **network** whose `config:` supplies `tools:`/`mcp:`, this reverses:
|
|
326
|
+
> the parent level is the network's list, and build-time `:inherit` is what
|
|
327
|
+
> makes the robot pick it up. See
|
|
328
|
+
> [Network-Wide Tool and MCP Defaults](creating-networks.md#network-wide-tool-and-mcp-defaults).
|
|
329
|
+
|
|
225
330
|
## MCP in Networks
|
|
226
331
|
|
|
227
|
-
When running robots in a network, use per-task MCP configuration
|
|
332
|
+
When running robots in a network, use per-task MCP configuration. Remember that
|
|
333
|
+
a task's `mcp:`/`tools:` become that robot's **runtime** values, so `tools:`
|
|
334
|
+
must be set too or the MCP tools will not be sent:
|
|
228
335
|
|
|
229
336
|
```ruby
|
|
230
337
|
network = RobotLab.create_network(name: "dev_pipeline") do
|
|
@@ -233,6 +340,7 @@ network = RobotLab.create_network(name: "dev_pipeline") do
|
|
|
233
340
|
mcp: [
|
|
234
341
|
{ name: "filesystem", transport: { type: "stdio", command: "mcp-server-fs" } }
|
|
235
342
|
],
|
|
343
|
+
tools: :inherit,
|
|
236
344
|
depends_on: [:planner]
|
|
237
345
|
task :reviewer, reviewer_robot, depends_on: [:coder]
|
|
238
346
|
end
|
|
@@ -310,6 +418,13 @@ client.list_resources # => Array of resource definitions
|
|
|
310
418
|
client.disconnect
|
|
311
419
|
```
|
|
312
420
|
|
|
421
|
+
> [!NOTE]
|
|
422
|
+
> `MCP::Client#initialize(server_or_config, poller: nil)` takes its server as a
|
|
423
|
+
> **positional** argument — either a `Server` instance or a config Hash.
|
|
424
|
+
> `Client.new(name: "x", transport: {...})` raises `ArgumentError: wrong number
|
|
425
|
+
> of arguments (given 0, expected 1)`; wrap the hash in braces:
|
|
426
|
+
> `Client.new({ name: "x", transport: {...} })`.
|
|
427
|
+
|
|
313
428
|
## Connection Multiplexing
|
|
314
429
|
|
|
315
430
|
When a robot connects to several local (stdio) MCP servers, each client normally blocks independently while waiting for a response. `MCP::ConnectionPoller` replaces this with a single `IO.select` call across all registered stdout file descriptors, dispatching each response to the pending request for that client.
|
|
@@ -377,8 +492,10 @@ robot = RobotLab.build(
|
|
|
377
492
|
]
|
|
378
493
|
)
|
|
379
494
|
|
|
380
|
-
# Discovery connects only :brew for this message — filesystem and github are skipped
|
|
381
|
-
|
|
495
|
+
# Discovery connects only :brew for this message — filesystem and github are skipped.
|
|
496
|
+
# mcp: :inherit is required; a plain run() resolves the MCP list to empty and
|
|
497
|
+
# discovery never runs.
|
|
498
|
+
robot.run("install imagemagick", mcp: :inherit, tools: :inherit)
|
|
382
499
|
```
|
|
383
500
|
|
|
384
501
|
### How It Works
|
|
@@ -387,7 +504,7 @@ robot.run("install imagemagick")
|
|
|
387
504
|
|
|
388
505
|
The threshold is intentionally low — server descriptions are short, so raw cosine scores are naturally small even for on-topic queries.
|
|
389
506
|
|
|
390
|
-
Discovery only applies on the **first** `run()` call (before
|
|
507
|
+
Discovery only applies on the **first** MCP-resolving `run()` call (before the robot is marked MCP-initialized). Once a set of servers is connected they remain connected for the robot's lifetime, preserving tool continuity across a conversation.
|
|
391
508
|
|
|
392
509
|
### Fallback Behaviour
|
|
393
510
|
|
|
@@ -404,23 +521,42 @@ All servers are returned unchanged when any of the following apply:
|
|
|
404
521
|
|
|
405
522
|
```ruby
|
|
406
523
|
servers = [
|
|
407
|
-
{ name: "filesystem", description: "Read and write files",
|
|
408
|
-
|
|
524
|
+
{ name: "filesystem", description: "Read and write files",
|
|
525
|
+
transport: { type: "stdio", command: "mcp-server-fs" } },
|
|
526
|
+
{ name: "github", description: "GitHub repos and PRs",
|
|
527
|
+
transport: { type: "stdio", command: "mcp-server-github" } }
|
|
409
528
|
]
|
|
410
529
|
|
|
411
530
|
relevant = RobotLab::MCP::ServerDiscovery.select(
|
|
412
|
-
"
|
|
531
|
+
"search github repos",
|
|
413
532
|
from: servers,
|
|
414
533
|
threshold: 0.05 # optional, default
|
|
415
534
|
)
|
|
416
|
-
# =>
|
|
535
|
+
# => [{ name: "github", ... }]
|
|
417
536
|
```
|
|
418
537
|
|
|
538
|
+
> [!IMPORTANT]
|
|
539
|
+
> Scoring is lexical, not semantic — it compares word stems against each
|
|
540
|
+
> server's `description`. A query that happens to share no stems with **any**
|
|
541
|
+
> description scores 0.0 everywhere, trips the "nothing above threshold"
|
|
542
|
+
> fallback in the table above, and gets **all** servers back rather than none.
|
|
543
|
+
> With the two servers above:
|
|
544
|
+
>
|
|
545
|
+
> ```ruby
|
|
546
|
+
> select("search github repos", from: servers) # => ["github"]
|
|
547
|
+
> select("read and write files", from: servers) # => ["filesystem"]
|
|
548
|
+
> select("list open pull requests", from: servers) # => ["filesystem", "github"] <- fallback
|
|
549
|
+
> ```
|
|
550
|
+
>
|
|
551
|
+
> The last one selects everything because the description says "PRs", not "pull
|
|
552
|
+
> requests". Write descriptions using the words your prompts will actually use.
|
|
553
|
+
|
|
419
554
|
## Connection Resilience
|
|
420
555
|
|
|
421
556
|
### Eager Connection
|
|
422
557
|
|
|
423
|
-
|
|
558
|
+
`connect_mcp!` connects the robot's configured servers immediately, without
|
|
559
|
+
waiting for a `run()` that passes `mcp: :inherit`:
|
|
424
560
|
|
|
425
561
|
```ruby
|
|
426
562
|
robot = RobotLab.build(
|
|
@@ -440,14 +576,22 @@ if robot.failed_mcp_server_names.any?
|
|
|
440
576
|
end
|
|
441
577
|
```
|
|
442
578
|
|
|
579
|
+
> [!NOTE]
|
|
580
|
+
> `connect_mcp!` only opens the connections. A later plain `robot.run(message)`
|
|
581
|
+
> still sends the LLM **zero** tools, because `run`'s `tools:` defaults to
|
|
582
|
+
> `:none`. Eager connection and tool visibility are separate switches.
|
|
583
|
+
|
|
443
584
|
### Automatic Retry
|
|
444
585
|
|
|
445
|
-
Failed MCP servers are
|
|
586
|
+
Failed MCP servers are retried on subsequent `run()` calls **that resolve to a
|
|
587
|
+
non-empty MCP list** — i.e. runs that pass `mcp: :inherit` (or an explicit
|
|
588
|
+
array). If a server was down when the robot first connected, it is retried
|
|
589
|
+
transparently:
|
|
446
590
|
|
|
447
591
|
```ruby
|
|
448
|
-
robot.run("First message")
|
|
592
|
+
robot.run("First message", mcp: :inherit, tools: :inherit) # github connects, filesystem fails
|
|
449
593
|
# ... filesystem comes back up ...
|
|
450
|
-
robot.run("Second message")
|
|
594
|
+
robot.run("Second message", mcp: :inherit, tools: :inherit) # filesystem retried and connects
|
|
451
595
|
```
|
|
452
596
|
|
|
453
597
|
### Injecting External MCP Clients
|
|
@@ -464,25 +608,45 @@ This skips the normal connection process and marks the robot as MCP-initialized.
|
|
|
464
608
|
|
|
465
609
|
### Connection Errors
|
|
466
610
|
|
|
611
|
+
MCP connection failures are **not raised**. They are logged as warnings and
|
|
612
|
+
recorded on the robot; the run continues without that server's tools, and one
|
|
613
|
+
failing server does not prevent the others from connecting. Inspect the result
|
|
614
|
+
rather than rescuing:
|
|
615
|
+
|
|
467
616
|
```ruby
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
617
|
+
result = robot.run("Search for repos", mcp: :inherit, tools: :inherit)
|
|
618
|
+
|
|
619
|
+
if robot.failed_mcp_server_names.any?
|
|
620
|
+
warn "MCP servers unavailable: #{robot.failed_mcp_server_names.join(', ')}"
|
|
472
621
|
end
|
|
473
622
|
```
|
|
474
623
|
|
|
475
|
-
MCP connection failures are logged as warnings but do not raise errors by default. The robot will continue without MCP tools if a server is unreachable. One failing server does not prevent other servers from connecting.
|
|
476
|
-
|
|
477
624
|
### Timeout Errors
|
|
478
625
|
|
|
479
|
-
Stdio transports wrap all blocking I/O with a configurable timeout.
|
|
626
|
+
Stdio transports wrap all blocking I/O with a configurable timeout. On expiry
|
|
627
|
+
the transport raises `MCPError`. Note that the two messages differ, and neither
|
|
628
|
+
uses the server *name* you configured — the handshake message names the
|
|
629
|
+
**command**, and the per-request message names nothing at all:
|
|
480
630
|
|
|
481
631
|
```ruby
|
|
482
|
-
#
|
|
483
|
-
# RobotLab::MCPError: MCP server 'heavy-server' did not respond within 15s
|
|
632
|
+
# Handshake timed out during connect:
|
|
633
|
+
# RobotLab::MCPError: MCP server 'heavy-mcp-server' did not respond within 15s
|
|
634
|
+
|
|
635
|
+
# A later request timed out:
|
|
636
|
+
# RobotLab::MCPError: MCP server did not respond within 15s
|
|
484
637
|
```
|
|
485
638
|
|
|
639
|
+
> [!IMPORTANT]
|
|
640
|
+
> `MCP::Client#connect` rescues **every** `StandardError`, logs
|
|
641
|
+
> `"MCP connection failed for <name>: ..."` at `:warn`, and returns `self` with
|
|
642
|
+
> `connected?` false. So a connect-time timeout never reaches your `begin/rescue`
|
|
643
|
+
> — check `client.connected?` (or `robot.failed_mcp_server_names`) instead.
|
|
644
|
+
> Per-request calls such as `list_tools` and `call_tool` **do** propagate
|
|
645
|
+
> `MCPError`.
|
|
646
|
+
|
|
647
|
+
SSE, WebSocket, and StreamableHTTP raise no timeout error at all — see the
|
|
648
|
+
warning under [Timeout Configuration](#timeout-configuration).
|
|
649
|
+
|
|
486
650
|
## Disconnecting
|
|
487
651
|
|
|
488
652
|
Robots can be manually disconnected from MCP servers:
|
|
@@ -546,25 +710,28 @@ robot = RobotLab.build(
|
|
|
546
710
|
|
|
547
711
|
### 2. Limit Tool Access
|
|
548
712
|
|
|
549
|
-
Restrict which MCP tools
|
|
713
|
+
Restrict which MCP tools reach the LLM with a `tools:` allowlist on the run:
|
|
550
714
|
|
|
551
715
|
```ruby
|
|
552
716
|
robot = RobotLab.build(
|
|
553
717
|
name: "reader",
|
|
554
718
|
system_prompt: "You read and search files.",
|
|
555
|
-
mcp: [{ name: "fs", transport: { type: "stdio", command: "mcp-fs" } }]
|
|
556
|
-
tools: %w[read_file search_files] # No write access
|
|
719
|
+
mcp: [{ name: "fs", transport: { type: "stdio", command: "mcp-fs" } }]
|
|
557
720
|
)
|
|
721
|
+
|
|
722
|
+
robot.run("Find the config file",
|
|
723
|
+
mcp: :inherit,
|
|
724
|
+
tools: %w[read_file search_files]) # No write access
|
|
558
725
|
```
|
|
559
726
|
|
|
560
727
|
### 3. Use Appropriate Transports
|
|
561
728
|
|
|
562
729
|
| Transport | Best For |
|
|
563
730
|
|-----------|----------|
|
|
564
|
-
| `stdio` | Local servers, CLI tools |
|
|
565
|
-
| `websocket` | Persistent connections, bidirectional |
|
|
731
|
+
| `stdio` | Local servers, CLI tools. The only transport that enforces `timeout:` and reports connect failures synchronously. |
|
|
732
|
+
| `websocket` (or `ws`) | Persistent connections, bidirectional |
|
|
566
733
|
| `sse` | Server push, event streams |
|
|
567
|
-
| `
|
|
734
|
+
| `streamable-http` (or `http`) | Remote APIs, session-based |
|
|
568
735
|
|
|
569
736
|
## Next Steps
|
|
570
737
|
|