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/api/support.md
ADDED
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
# Support Classes
|
|
2
|
+
|
|
3
|
+
Method-level reference for the classes that back RobotLab's core behaviors but
|
|
4
|
+
that you rarely construct yourself. The features they implement are explained in
|
|
5
|
+
the guides; this page is the API surface, so nothing public is left undocumented.
|
|
6
|
+
|
|
7
|
+
| Class | Backs | Guide |
|
|
8
|
+
|-------|-------|-------|
|
|
9
|
+
| [`Task`](#robotlabtask) | Per-task config in a network pipeline | [Creating Networks](../guides/creating-networks.md) |
|
|
10
|
+
| [`Runnable`](#robotlabrunnable) | The shared `Robot`/`Network` interface | [Core Concepts](../architecture/core-concepts.md#runnable-protocol) |
|
|
11
|
+
| [`ToolConfig`](#robotlabtoolconfig) | `:none` / `:inherit` / array resolution | [Using Tools](../guides/using-tools.md) |
|
|
12
|
+
| [`ToolManifest`](#robotlabtoolmanifest) | Name-keyed tool collection | [Using Tools](../guides/using-tools.md) |
|
|
13
|
+
| [`Budget::Ledger`](#robotlabbudgetledger) | `token_budget` / `cost_budget` | [Budgets](../guides/observability.md#budgets-token-cost) |
|
|
14
|
+
| [`DoomLoopDetector`](#robotlabdoomloopdetector) | Always-on tool-loop detection | [Doom Loop Detection](../guides/observability.md#doom-loop-detection) |
|
|
15
|
+
| [`HistoryCompressor`](#robotlabhistorycompressor) | `compress_history` / `auto_compact` | [Context Compression](../guides/observability.md#context-window-compression) |
|
|
16
|
+
| [`Convergence`](#robotlabconvergence) | Agreement detection between two texts | [Convergence Detection](../guides/observability.md#convergence-detection) |
|
|
17
|
+
| [`TextAnalysis`](#robotlabtextanalysis) | TF/TF-IDF primitives under all of the above | — |
|
|
18
|
+
| [`DelegationFuture`](#robotlabdelegationfuture) | `delegate(async: true)` | [Structured Delegation](../guides/observability.md#structured-delegation) |
|
|
19
|
+
| [`RobotMessage`](#robotlabrobotmessage) | Bus message envelope | [Core Concepts](../architecture/core-concepts.md) |
|
|
20
|
+
| [`BusPoller`](#robotlabbuspoller) | Per-robot bus delivery serialization | [Creating Networks](../guides/creating-networks.md) |
|
|
21
|
+
| [`Waiter`](#robotlabwaiter) | `memory.get(wait:)` blocking reads | [Memory](../guides/memory.md) |
|
|
22
|
+
| [`Narrator`](#robotlabnarrator) | Live console narration | [Live Narration](../guides/observability.md#live-narration-robotlabnarrator) |
|
|
23
|
+
| [`Config`](#robotlabconfig) | `RobotLab.config` | [Configuration](../getting-started/configuration.md) |
|
|
24
|
+
| [`MCP::ServerDiscovery`](#robotlabmcpserverdiscovery) | `mcp_discovery: true` | [MCP Integration](../guides/mcp-integration.md) |
|
|
25
|
+
| [`MCP::ConnectionPoller`](#robotlabmcpconnectionpoller) | Multiplexed stdio MCP I/O | [Transports](mcp/transports.md) |
|
|
26
|
+
| [`Streaming::SequenceCounter`](#robotlabstreamingsequencecounter) | Event ordering (unused by core) | [Streaming](streaming/index.md) |
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## RobotLab::Task
|
|
31
|
+
|
|
32
|
+
Wraps a `Robot` as a SimpleFlow pipeline step, carrying per-task context, MCP,
|
|
33
|
+
tools, memory, and config. `Network#task` builds one for you — you would only
|
|
34
|
+
construct one directly when driving a `SimpleFlow::Pipeline` yourself.
|
|
35
|
+
|
|
36
|
+
### Constructor
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
RobotLab::Task.new(name:, robot:, context: {}, mcp: :none, tools: :none,
|
|
40
|
+
memory: nil, config: nil, network: nil)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
| Name | Type | Default | Description |
|
|
44
|
+
|------|------|---------|-------------|
|
|
45
|
+
| `name` | `Symbol` | **required** | Task name; symbolized |
|
|
46
|
+
| `robot` | `Robot` | **required** | The robot to execute |
|
|
47
|
+
| `context` | `Hash` | `{}` | Deep-merged over the run params (nested Hashes merge recursively; Arrays are replaced, not concatenated) |
|
|
48
|
+
| `mcp` | `Symbol`, `Array` | `:none` | Injected into `run_params[:mcp]` — only when not `:none` |
|
|
49
|
+
| `tools` | `Symbol`, `Array` | `:none` | Injected into `run_params[:tools]` — only when not `:none` |
|
|
50
|
+
| `memory` | `Memory`, `Hash`, `nil` | `nil` | Overrides the network's shared memory for this task |
|
|
51
|
+
| `config` | `RunConfig`, `nil` | `nil` | Merged **on top of** any inherited `network_config` |
|
|
52
|
+
| `network` | `Network`, `nil` | `nil` | Owning network; supplies `hooks` and default `memory` |
|
|
53
|
+
|
|
54
|
+
### name / robot
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
task.name # => Symbol
|
|
58
|
+
task.robot # => Robot
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### call
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
task.call(result) # => SimpleFlow::Result
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The SimpleFlow step interface. Builds a `TaskHookContext`, runs the `:task` hook
|
|
68
|
+
family against `[RobotLab.hooks, network&.hooks]`, and inside it calls
|
|
69
|
+
`robot.call(enhanced_result)` with the task's configuration merged into
|
|
70
|
+
`run_params`.
|
|
71
|
+
|
|
72
|
+
!!! warning "The `:task` family never sees a robot's own registry"
|
|
73
|
+
Registries are `[RobotLab.hooks, @network&.hooks]` — `robot.hooks` is not
|
|
74
|
+
consulted. A `before_task`/`around_task`/`after_task`/`on_error` handler
|
|
75
|
+
registered with `robot.on` will never fire; use `RobotLab.on` or `network.on`.
|
|
76
|
+
|
|
77
|
+
### to_h
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
task.to_h
|
|
81
|
+
# => { name: :billing, robot: "billing_bot", context: {...}, mcp: :none, tools: :none }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`.compact`ed. `memory` appears as `true` when a task-specific memory was supplied
|
|
85
|
+
and is omitted otherwise; the memory object itself is never serialized. `config`
|
|
86
|
+
and `network` are not included at all.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## RobotLab::Runnable
|
|
91
|
+
|
|
92
|
+
The shared interface `Robot` and `Network` both implement, so callers can treat
|
|
93
|
+
either uniformly instead of branching on `is_a?(RobotLab::Network)`.
|
|
94
|
+
|
|
95
|
+
**Implementers must provide** `#run(message = nil, **opts)`, `#crew`, and
|
|
96
|
+
optionally override `#network?`. The rest derive.
|
|
97
|
+
|
|
98
|
+
| Method | Default implementation | `Robot` | `Network` |
|
|
99
|
+
|--------|------------------------|---------|-----------|
|
|
100
|
+
| `crew` | raises `NotImplementedError` | `[self]` | `robots.values` |
|
|
101
|
+
| `chief` | `crew.first` | the robot | first robot in pipeline order |
|
|
102
|
+
| `robot_count` | `crew.size` | `1` | number of robots |
|
|
103
|
+
| `network?` | `false` | `false` | `true` |
|
|
104
|
+
| `single?` | `!network?` | `true` | `false` |
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
def summarize(runnable)
|
|
108
|
+
runnable.run(prompt, mcp: :inherit, tools: :inherit)
|
|
109
|
+
puts "#{runnable.robot_count} robot(s): #{runnable.crew.map(&:name).join(', ')}"
|
|
110
|
+
puts "network!" if runnable.network?
|
|
111
|
+
end
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
!!! note "`crew` returns robot instances, not `network.robots` keys"
|
|
115
|
+
`network.crew.map(&:name)` yields each **robot's** name, which differs from
|
|
116
|
+
`network.robots.keys` (task names) unless you name each robot after its task.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## RobotLab::ToolConfig
|
|
121
|
+
|
|
122
|
+
Resolves the `:none` / `:inherit` / array values used at every level of the tools
|
|
123
|
+
and MCP hierarchy. Module functions.
|
|
124
|
+
|
|
125
|
+
### NONE_VALUES
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
RobotLab::ToolConfig::NONE_VALUES # => [nil, [], :none]
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
All three mean "nothing at this level".
|
|
132
|
+
|
|
133
|
+
### resolve
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
RobotLab::ToolConfig.resolve(value, parent_value:) # => Array
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
| `value` | Result |
|
|
140
|
+
|---------|--------|
|
|
141
|
+
| `:inherit` | `Array(parent_value)` |
|
|
142
|
+
| `nil`, `[]`, `:none` | `[]` |
|
|
143
|
+
| anything else | `Array(value)` |
|
|
144
|
+
|
|
145
|
+
### resolve_mcp / resolve_tools
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
RobotLab::ToolConfig.resolve_mcp(value, parent_value:) # => Array
|
|
149
|
+
RobotLab::ToolConfig.resolve_tools(value, parent_value:) # => Array<String>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`resolve_mcp` is `resolve` verbatim (server configs stay as-is).
|
|
153
|
+
`resolve_tools` additionally maps every entry through `to_s`, which is why a
|
|
154
|
+
tools allowlist is matched against `tool.name.to_s` and a Symbol entry works
|
|
155
|
+
interchangeably with a String.
|
|
156
|
+
|
|
157
|
+
### none_value? / inherit_value?
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
RobotLab::ToolConfig.none_value?(:none) # => true
|
|
161
|
+
RobotLab::ToolConfig.none_value?([]) # => true
|
|
162
|
+
RobotLab::ToolConfig.inherit_value?(:inherit) # => true
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`Robot#build_effective_config` uses `none_value?` to decide whether an
|
|
166
|
+
`mcp:`/`tools:` kwarg is worth storing on the `RunConfig` at all.
|
|
167
|
+
|
|
168
|
+
### filter_tools
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
RobotLab::ToolConfig.filter_tools(tools, allowed_names: %w[order_lookup])
|
|
172
|
+
# => Array — the subset whose names match
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
!!! warning "An empty allowlist means *no* tools here"
|
|
176
|
+
`filter_tools(tools, allowed_names: [])` returns `[]`, **not** all tools.
|
|
177
|
+
`Robot#filtered_tools` short-circuits before reaching this method when the
|
|
178
|
+
allowlist is empty (treating it as "no filter"), which is why `Robot#run`
|
|
179
|
+
has to branch on the *raw* runtime value to honor an explicit `tools: :none`.
|
|
180
|
+
See [Robot: run](core/robot.md#run).
|
|
181
|
+
|
|
182
|
+
Names are extracted with `tool.name.to_s` for objects, and used verbatim for
|
|
183
|
+
String/Symbol entries — so a class-attached tool matches `"RefundTool"` while an
|
|
184
|
+
instance-attached one matches its declared `"refund"`.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## RobotLab::ToolManifest
|
|
189
|
+
|
|
190
|
+
A name-keyed collection of tools, including `Enumerable`. Not used on the hot
|
|
191
|
+
path by `Robot` (which keeps plain Arrays), but available for applications
|
|
192
|
+
managing tool catalogs.
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
manifest = RobotLab::ToolManifest.new([weather_tool, calculator_tool])
|
|
196
|
+
manifest[:get_weather] # => Tool
|
|
197
|
+
manifest.names # => ["get_weather", "calculate"]
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Tools are keyed by `tool.name.to_s`, and every lookup coerces its argument with
|
|
201
|
+
`to_s` — so `manifest[:get_weather]` and `manifest["get_weather"]` are the same
|
|
202
|
+
entry.
|
|
203
|
+
|
|
204
|
+
| Method | Returns | Description |
|
|
205
|
+
|--------|---------|-------------|
|
|
206
|
+
| `ToolManifest.new(tools = [])` | `ToolManifest` | Each tool is added via `add` |
|
|
207
|
+
| `add(tool)` | `self` | Register a tool under its `name`. Aliased as `<<` |
|
|
208
|
+
| `[](name)` | `Tool`, `nil` | Lookup; `nil` when absent |
|
|
209
|
+
| `fetch(name)` | `Tool` | Lookup, but **raises `RobotLab::ToolNotFoundError`** listing the available names |
|
|
210
|
+
| `include?(name)` | `Boolean` | Whether that name is registered. Aliased as `has?` |
|
|
211
|
+
| `remove(name)` | `Tool`, `nil` | Delete and return the tool |
|
|
212
|
+
| `replace(tools)` | `self` | **Clears the manifest** and adds `tools` — replaces the whole collection, not one entry |
|
|
213
|
+
| `merge(other)` | `self` | Add every tool from a `ToolManifest`, an `Array`, or a single `Tool`. Mutates the receiver and returns it — it does **not** build a new manifest |
|
|
214
|
+
| `names` | `Array<String>` | Registered names |
|
|
215
|
+
| `values` | `Array<Tool>` | The tools themselves. Aliased as `all` and `to_a` |
|
|
216
|
+
| `each { \|tool\| }` | — | `Enumerable` entry point (yields tools, not pairs) |
|
|
217
|
+
| `size` | `Integer` | Number of tools. Aliased as `count` and `length` |
|
|
218
|
+
| `empty?` | `Boolean` | |
|
|
219
|
+
| `clear` | `self` | Remove everything |
|
|
220
|
+
| `to_h` | `Hash<String, Hash>` | Name => `tool.to_h` |
|
|
221
|
+
| `to_json` | `String` | Serializes `to_h` |
|
|
222
|
+
| `ToolManifest.from_hash(hash)` | `ToolManifest` | Rebuilds each entry with `Tool.create(name:, description:, parameters:, &handler)` |
|
|
223
|
+
|
|
224
|
+
!!! warning "`from_hash` is not the inverse of `to_h`"
|
|
225
|
+
`to_h` emits `Tool#to_h` (`name`, `description`, `mcp`), while `from_hash`
|
|
226
|
+
expects each value to carry `:parameters` and a `:handler` Proc. Round-tripping
|
|
227
|
+
a manifest through `to_h` → `from_hash` yields tools with no parameters and a
|
|
228
|
+
`nil` handler.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## RobotLab::Budget::Ledger
|
|
233
|
+
|
|
234
|
+
Thread-safe reserve/reconcile ledger tracking consumption against per-dimension
|
|
235
|
+
limits. `Robot` builds one automatically when `token_budget:` and/or
|
|
236
|
+
`cost_budget:` is configured, and exposes it as `robot.budget_ledger` (`nil`
|
|
237
|
+
otherwise).
|
|
238
|
+
|
|
239
|
+
The reserve-then-reconcile shape exists because an LLM call's size is unknowable
|
|
240
|
+
in advance: `reserve!` claims everything still available so an already-exhausted
|
|
241
|
+
budget is caught *before* spending, and `reconcile!` swaps that claim for the
|
|
242
|
+
actual usage once the response is back.
|
|
243
|
+
|
|
244
|
+
### Constructor
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
ledger = RobotLab::Budget::Ledger.new(limits: { tokens: 10_000, cost: 0.50 }, consumed: {})
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
| Name | Type | Default | Description |
|
|
251
|
+
|------|------|---------|-------------|
|
|
252
|
+
| `limits` | `Hash{Symbol=>Numeric}` | `{}` | Per-dimension ceilings. A dimension **absent** here is unlimited |
|
|
253
|
+
| `consumed` | `Hash{Symbol=>Numeric}` | `{}` | Starting consumption, e.g. restored from a prior session |
|
|
254
|
+
|
|
255
|
+
`Robot` uses the dimensions `:tokens` and `:cost`, but the ledger is generic —
|
|
256
|
+
any Symbol works.
|
|
257
|
+
|
|
258
|
+
### limits / consumed
|
|
259
|
+
|
|
260
|
+
```ruby
|
|
261
|
+
ledger.limits # => { tokens: 10_000, cost: 0.5 }
|
|
262
|
+
ledger.consumed # => { tokens: 3_412, cost: 0.081 }
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Actual consumption so far, per dimension. `consumed` is backed by a
|
|
266
|
+
`Hash.new(0)`, so an untouched dimension reads `0` rather than `nil`.
|
|
267
|
+
|
|
268
|
+
### reserve!
|
|
269
|
+
|
|
270
|
+
```ruby
|
|
271
|
+
ledger.reserve!(:tokens, 6_588)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Claim `amount` against the remaining budget.
|
|
275
|
+
|
|
276
|
+
**Raises `RobotLab::BudgetExceeded`** — `"budget exceeded for tokens: 10588 > 10000"`
|
|
277
|
+
— when `consumed + already_reserved + amount` would exceed the limit. A **no-op
|
|
278
|
+
that never raises** for a dimension with no configured limit.
|
|
279
|
+
|
|
280
|
+
### reconcile!
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
ledger.reconcile!(:tokens, reserved_amount, actual_amount)
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Release `reserved_amount` from the reservation pool and add `actual_amount` to
|
|
287
|
+
`consumed`. The actual may be larger or smaller than what was reserved. The
|
|
288
|
+
reservation pool is floored at zero, so an over-release cannot make it negative.
|
|
289
|
+
|
|
290
|
+
### release!
|
|
291
|
+
|
|
292
|
+
```ruby
|
|
293
|
+
ledger.release!(:tokens, amount)
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Drop a reservation **without** recording consumption — for work that was reserved
|
|
297
|
+
and then skipped.
|
|
298
|
+
|
|
299
|
+
### remaining
|
|
300
|
+
|
|
301
|
+
```ruby
|
|
302
|
+
ledger.remaining(:tokens) # => Numeric
|
|
303
|
+
ledger.remaining(:unmetered) # => Float::INFINITY
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
`limit - consumed - reserved`, floored at `0`. Returns `Float::INFINITY` for a
|
|
307
|
+
dimension with no configured limit.
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## RobotLab::DoomLoopDetector
|
|
312
|
+
|
|
313
|
+
Detects a model stuck calling the same tool — or the same *cycle* of tools — over
|
|
314
|
+
and over. `Robot#run` installs one on every call; see
|
|
315
|
+
[Doom Loop Detection](core/robot.md#doom-loop-detection).
|
|
316
|
+
|
|
317
|
+
### Constants
|
|
318
|
+
|
|
319
|
+
| Constant | Value | Description |
|
|
320
|
+
|----------|-------|-------------|
|
|
321
|
+
| `DEFAULT_THRESHOLD` | `3` | Repetitions before a loop is declared |
|
|
322
|
+
| `MAX_PERIOD` | `10` | Longest cyclic pattern searched for |
|
|
323
|
+
|
|
324
|
+
### Constructor / sequence
|
|
325
|
+
|
|
326
|
+
```ruby
|
|
327
|
+
detector = RobotLab::DoomLoopDetector.new(threshold: 3)
|
|
328
|
+
detector.sequence # => Array<String> — every tracked name, in order
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### track
|
|
332
|
+
|
|
333
|
+
```ruby
|
|
334
|
+
detector.track("search") # => the sequence array
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Append a tool name (coerced with `to_s`). Call once per tool invocation.
|
|
338
|
+
|
|
339
|
+
### doom_loop?
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
detector.doom_loop? # => Boolean
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
`true` when either pattern is present in the tail of the sequence:
|
|
346
|
+
|
|
347
|
+
- **Consecutive** — the last `threshold` entries are all the same name (`A, A, A`).
|
|
348
|
+
- **Cyclic** — the last `threshold × period` entries are exactly `period`-length
|
|
349
|
+
pattern repeated `threshold` times (`A,B,C, A,B,C, A,B,C`), for any period from
|
|
350
|
+
2 up to `min(MAX_PERIOD, sequence.length / threshold)`.
|
|
351
|
+
|
|
352
|
+
Always `false` while fewer than `threshold` calls have been tracked.
|
|
353
|
+
|
|
354
|
+
### warning_message
|
|
355
|
+
|
|
356
|
+
```ruby
|
|
357
|
+
detector.warning_message # => String
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
The self-correction text embedded in the tool result. `""` for an empty sequence.
|
|
361
|
+
Consecutive loops name the tool; cyclic loops render the pattern as
|
|
362
|
+
`"A → B → C"`. Both close with the same advice to try a fundamentally different
|
|
363
|
+
approach or ask for clarification.
|
|
364
|
+
|
|
365
|
+
### reset
|
|
366
|
+
|
|
367
|
+
```ruby
|
|
368
|
+
detector.reset # => []
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Clear the sequence. `Robot` calls this immediately after emitting a warning, so
|
|
372
|
+
the same loop is reported once rather than on every subsequent call.
|
|
373
|
+
|
|
374
|
+
!!! note "The detector never raises"
|
|
375
|
+
It only appends a warning to the tool's result — a `String` result gets
|
|
376
|
+
`"\n\n⚠️ <warning>"`, a `Hash` result gains a `:_doom_loop_warning` key. To
|
|
377
|
+
make a loop *fatal*, use `max_tool_rounds:` and its `ToolLoopError`.
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## RobotLab::HistoryCompressor
|
|
382
|
+
|
|
383
|
+
The algorithm behind [`robot.compress_history`](core/robot.md#compress_history)
|
|
384
|
+
and `auto_compact: :context_window`. Requires the optional `classifier` gem
|
|
385
|
+
(`~> 2.3`).
|
|
386
|
+
|
|
387
|
+
### Constructor
|
|
388
|
+
|
|
389
|
+
```ruby
|
|
390
|
+
RobotLab::HistoryCompressor.new(
|
|
391
|
+
messages:, recent_turns:, keep_threshold:, drop_threshold:, summarizer:
|
|
392
|
+
)
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
All five are **required** keywords — the defaults you see documented
|
|
396
|
+
(`recent_turns: 3`, `keep_threshold: 0.6`, `drop_threshold: 0.2`,
|
|
397
|
+
`summarizer: nil`) live on `Robot#compress_history`, not here.
|
|
398
|
+
|
|
399
|
+
**Raises `ArgumentError`** when `keep_threshold <= drop_threshold`.
|
|
400
|
+
|
|
401
|
+
### call
|
|
402
|
+
|
|
403
|
+
```ruby
|
|
404
|
+
compressed = compressor.call # => Array — the new message array
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Returns the input **unchanged** — without requiring the `classifier` gem — in any
|
|
408
|
+
of these cases: no messages; nothing scorable; the scorable messages all fit
|
|
409
|
+
inside the recent window (`scorable.size <= recent_turns * 2`); or the recent
|
|
410
|
+
window yields no text long enough to build a reference vector.
|
|
411
|
+
|
|
412
|
+
Otherwise, it builds a mean term-frequency vector from the recent window and
|
|
413
|
+
scores each older message against it:
|
|
414
|
+
|
|
415
|
+
| Score | Action |
|
|
416
|
+
|-------|--------|
|
|
417
|
+
| `>= keep_threshold` | Kept verbatim |
|
|
418
|
+
| `drop_threshold ... keep_threshold` | Passed to `summarizer`; **dropped** when there is no summarizer, or the summary comes back blank |
|
|
419
|
+
| `< drop_threshold` | Dropped |
|
|
420
|
+
| text shorter than `MIN_SCORE_LENGTH` (20 chars) | Kept — too short to score reliably |
|
|
421
|
+
|
|
422
|
+
**Pinned messages are never scored or removed:** system messages, `:tool` and
|
|
423
|
+
`:tool_result` messages, and any assistant message with blank content (a tool-call
|
|
424
|
+
dispatcher — removing one would orphan its `tool_result`).
|
|
425
|
+
|
|
426
|
+
Scoring uses **stemmed term frequencies without IDF**. IDF on a topic-focused
|
|
427
|
+
corpus suppresses exactly the shared terms that signal relevance, so it would
|
|
428
|
+
invert the ranking.
|
|
429
|
+
|
|
430
|
+
### MIN_SCORE_LENGTH
|
|
431
|
+
|
|
432
|
+
```ruby
|
|
433
|
+
RobotLab::HistoryCompressor::MIN_SCORE_LENGTH # => 20
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### SUMMARY_STRUCT
|
|
437
|
+
|
|
438
|
+
A minimal `Struct(:role, :content, :tool_calls, :stop_reason)` that duck-types
|
|
439
|
+
enough of `RubyLLM::Message` to sit in a chat's message array: `text?` (always
|
|
440
|
+
`true`), `tool_use?` (always `false`), `system?`, `user?`, `assistant?`. A
|
|
441
|
+
summarized message keeps its **original role**, so user/assistant turn ordering
|
|
442
|
+
survives compression.
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## RobotLab::Convergence
|
|
447
|
+
|
|
448
|
+
Whether two texts have converged on the same conclusion — for skipping a
|
|
449
|
+
reconciler call when two verifiers already agree. Requires the `classifier` gem.
|
|
450
|
+
|
|
451
|
+
### Constants
|
|
452
|
+
|
|
453
|
+
| Constant | Value | Description |
|
|
454
|
+
|----------|-------|-------------|
|
|
455
|
+
| `DEFAULT_THRESHOLD` | `0.85` | Similarity at or above which texts are convergent |
|
|
456
|
+
| `MIN_TEXT_LENGTH` | `30` | Characters; shorter texts always score `0.0` |
|
|
457
|
+
|
|
458
|
+
### detected?
|
|
459
|
+
|
|
460
|
+
```ruby
|
|
461
|
+
RobotLab::Convergence.detected?(text_a, text_b, threshold: 0.85) # => Boolean
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
**Raises `ArgumentError`** when `threshold` is outside `[0.0, 1.0]`, and
|
|
465
|
+
`RobotLab::DependencyError` when the `classifier` gem is missing.
|
|
466
|
+
|
|
467
|
+
### similarity
|
|
468
|
+
|
|
469
|
+
```ruby
|
|
470
|
+
RobotLab::Convergence.similarity(text_a, text_b) # => Float in [0.0, 1.0]
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Stemmed term-frequency cosine similarity. Returns `0.0` when **either** text is
|
|
474
|
+
shorter than `MIN_TEXT_LENGTH` after stripping — so two short but identical
|
|
475
|
+
strings score zero, not one.
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
## RobotLab::TextAnalysis
|
|
480
|
+
|
|
481
|
+
The shared TF/TF-IDF primitives that `Convergence`, `HistoryCompressor`,
|
|
482
|
+
`Robot#search_history`, and `MCP::ServerDiscovery` all sit on. Module functions.
|
|
483
|
+
|
|
484
|
+
| Method | Returns | Description |
|
|
485
|
+
|--------|---------|-------------|
|
|
486
|
+
| `require_classifier!` | — | Loads the `classifier` gem, converting `LoadError` into a `RobotLab::DependencyError` with install instructions. Call it before any other method |
|
|
487
|
+
| `load_classifier_gem` | — | The bare `require "classifier"`, extracted for testability |
|
|
488
|
+
| `fit(corpus)` | `Classifier::TFIDF` | Fit a TF-IDF model (`min_df: 1`) over an Array of document strings |
|
|
489
|
+
| `transform(model, text)` | `Hash{Symbol=>Float}` | L2-normalized sparse term vector; `{}` when no known terms |
|
|
490
|
+
| `cosine_similarity(vec_a, vec_b)` | `Float` in `[0.0, 1.0]` | Dot product (vectors are already normalized), clamped at `1.0`; `0.0` if either is empty |
|
|
491
|
+
| `dot(vec_a, vec_b)` | `Float` | Dot product over shared keys only |
|
|
492
|
+
| `l2_normalize(vec)` | `Hash{Symbol=>Float}` | Normalize a sparse vector; `{}` when the magnitude is zero |
|
|
493
|
+
| `tf_cosine_similarity(text_a, text_b)` | `Float` in `[0.0, 1.0]` | Stemmed term-frequency cosine between two texts — no reference corpus needed, which is why it, not TF-IDF, is used for 2-text comparison |
|
|
494
|
+
|
|
495
|
+
Only `tf_cosine_similarity` calls `require_classifier!` for you; the others assume
|
|
496
|
+
the gem is already loaded.
|
|
497
|
+
|
|
498
|
+
---
|
|
499
|
+
|
|
500
|
+
## RobotLab::DelegationFuture
|
|
501
|
+
|
|
502
|
+
The promise returned by `robot.delegate(to:, task:, async: true)`. See
|
|
503
|
+
[Robot: delegate](core/robot.md#delegate).
|
|
504
|
+
|
|
505
|
+
### Attributes
|
|
506
|
+
|
|
507
|
+
```ruby
|
|
508
|
+
future.robot_name # => "analyst" — the delegatee
|
|
509
|
+
future.delegated_by # => "manager" — the delegator
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### resolved?
|
|
513
|
+
|
|
514
|
+
```ruby
|
|
515
|
+
future.resolved? # => Boolean
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
`true` once the task finished, **whether it succeeded or raised**. Use it to poll
|
|
519
|
+
without blocking.
|
|
520
|
+
|
|
521
|
+
### value / wait
|
|
522
|
+
|
|
523
|
+
```ruby
|
|
524
|
+
result = future.value # blocks indefinitely
|
|
525
|
+
result = future.value(timeout: 30) # blocks up to 30s
|
|
526
|
+
result = future.wait # alias for value
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
Returns the delegatee's `RobotResult`, with `duration` and `delegated_by` already
|
|
530
|
+
set by `delegate`.
|
|
531
|
+
|
|
532
|
+
**Raises `RobotLab::DelegationFuture::DelegationTimeout`** (`"Delegation to 'X'
|
|
533
|
+
timed out after Ns"`) when `timeout:` expires, and **re-raises** whatever the
|
|
534
|
+
delegated task raised. The error is re-raised on every subsequent `value` call —
|
|
535
|
+
the future stays in its failed state.
|
|
536
|
+
|
|
537
|
+
### resolve! / reject!
|
|
538
|
+
|
|
539
|
+
```ruby
|
|
540
|
+
future.resolve!(robot_result)
|
|
541
|
+
future.reject!(exception)
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
Called by `Robot#delegate` from the worker thread to settle the future; both
|
|
545
|
+
broadcast to every blocked `value`. You would only call these when building a
|
|
546
|
+
custom delegation path — settling a future twice silently overwrites the first
|
|
547
|
+
outcome.
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
## RobotLab::RobotMessage
|
|
552
|
+
|
|
553
|
+
Immutable `Data` envelope for TypedBus inter-robot messaging.
|
|
554
|
+
|
|
555
|
+
### build
|
|
556
|
+
|
|
557
|
+
```ruby
|
|
558
|
+
msg = RobotLab::RobotMessage.build(id: 1, from: "alice", content: "Hello")
|
|
559
|
+
reply = RobotLab::RobotMessage.build(id: 2, from: "bob", content: "Hi",
|
|
560
|
+
in_reply_to: "alice:1")
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
Prefer `build` over `new` — it defaults `in_reply_to` to `nil`, which
|
|
564
|
+
`Data.define` does not do for you.
|
|
565
|
+
|
|
566
|
+
| Member | Type | Description |
|
|
567
|
+
|--------|------|-------------|
|
|
568
|
+
| `id` | `Integer` | The **sender's** per-robot counter, not globally unique |
|
|
569
|
+
| `from` | `String` | Sender's robot name, which is also its channel name |
|
|
570
|
+
| `content` | `String`, `Hash` | The payload |
|
|
571
|
+
| `in_reply_to` | `String`, `nil` | The `key` of the message being answered |
|
|
572
|
+
|
|
573
|
+
### key / reply?
|
|
574
|
+
|
|
575
|
+
```ruby
|
|
576
|
+
msg.key # => "alice:1" — "#{from}:#{id}"
|
|
577
|
+
msg.reply? # => !in_reply_to.nil?
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
`key` is the composite identity used for reply correlation: pass it as
|
|
581
|
+
`in_reply_to:` when answering, and the sender's `outbox[key]` entry flips to
|
|
582
|
+
`status: :replied`. `reply?` is what lets `respond_to_tasks` ignore replies and
|
|
583
|
+
avoid an infinite ping-pong between two robots.
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
## RobotLab::BusPoller
|
|
588
|
+
|
|
589
|
+
Serializes bus deliveries per robot. A `Network` creates one and shares it across
|
|
590
|
+
its tasks; a robot with a bus but no network auto-creates a private one.
|
|
591
|
+
|
|
592
|
+
!!! warning "Despite the name, there is no poller thread"
|
|
593
|
+
`start` and `stop` are **no-ops**, `running?` is hard-coded `true`, and
|
|
594
|
+
`enqueue` processes and drains **inline in the caller's own execution
|
|
595
|
+
context** (Async fiber or OS thread). All the class owns is a mutex and a
|
|
596
|
+
per-robot queue. Deliveries make no progress on their own while the calling
|
|
597
|
+
fiber is parked.
|
|
598
|
+
|
|
599
|
+
### QUEUE_CAPACITY
|
|
600
|
+
|
|
601
|
+
```ruby
|
|
602
|
+
RobotLab::BusPoller::QUEUE_CAPACITY # => 512
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
Capacity of each robot's `RactorQueue` of pending deliveries.
|
|
606
|
+
|
|
607
|
+
### enqueue
|
|
608
|
+
|
|
609
|
+
```ruby
|
|
610
|
+
poller.enqueue(robot:, delivery:, group: :default) # => void
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
If the robot is idle, marks it busy and processes the delivery immediately, then
|
|
614
|
+
drains anything that queued up behind it. If the robot is already processing, the
|
|
615
|
+
delivery is pushed onto its queue instead.
|
|
616
|
+
|
|
617
|
+
Errors are contained: a `BusError` (or any `StandardError`) from a handler
|
|
618
|
+
releases the robot's busy flag and is logged at `warn` rather than propagating to
|
|
619
|
+
the publisher.
|
|
620
|
+
|
|
621
|
+
### add_group / groups
|
|
622
|
+
|
|
623
|
+
```ruby
|
|
624
|
+
poller.add_group(:slow) # idempotent
|
|
625
|
+
poller.groups # => [:default, :slow]
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
Poller groups are **informational labels only** — they create no separate queues
|
|
629
|
+
or threads. `Network#task`'s `poller_group:` registers the name here so slow
|
|
630
|
+
robots are identifiable in logs and monitoring.
|
|
631
|
+
|
|
632
|
+
### start / stop / running?
|
|
633
|
+
|
|
634
|
+
```ruby
|
|
635
|
+
poller.start # => self (no-op)
|
|
636
|
+
poller.stop # => self (no-op; accepts and ignores any args)
|
|
637
|
+
poller.running? # => true (always)
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
Retained for API symmetry with the earlier threaded design.
|
|
641
|
+
|
|
642
|
+
---
|
|
643
|
+
|
|
644
|
+
## RobotLab::Waiter
|
|
645
|
+
|
|
646
|
+
The blocking primitive behind `memory.get(key, wait:)`. Built on an `IO.pipe`
|
|
647
|
+
pair rather than a `ConditionVariable`, because `IO#wait_readable` yields
|
|
648
|
+
correctly to the Async fiber scheduler while `ConditionVariable#wait` can block
|
|
649
|
+
the whole event loop.
|
|
650
|
+
|
|
651
|
+
| Method | Returns | Description |
|
|
652
|
+
|--------|---------|-------------|
|
|
653
|
+
| `Waiter.new` | `Waiter` | Allocates the pipe pair |
|
|
654
|
+
| `wait(timeout: nil)` | value, or `:timeout` | Blocks until signaled. `nil` timeout waits forever. Returns immediately when already signaled. A closed pipe (`IOError`) also yields `:timeout` |
|
|
655
|
+
| `signal(value)` | — | Stores `value` and wakes every waiter, writing one byte per blocked thread (minimum one, to cover a thread that passed the signaled check but has not yet entered the wait) |
|
|
656
|
+
| `signaled?` | `Boolean` | Whether `signal` has been called |
|
|
657
|
+
| `close` | — | Releases both file descriptors. Call after `wait` returns |
|
|
658
|
+
|
|
659
|
+
Multiple threads may wait on one instance. `Memory#wait_for_key` handles the
|
|
660
|
+
create/wait/close lifecycle and converts a `:timeout` return into
|
|
661
|
+
`RobotLab::AwaitTimeout`.
|
|
662
|
+
|
|
663
|
+
---
|
|
664
|
+
|
|
665
|
+
## RobotLab::Narrator
|
|
666
|
+
|
|
667
|
+
The one `RobotLab::Hook` subclass shipped in core: a human-facing console feed of
|
|
668
|
+
what a robot is doing, complementing the persistent record kept by
|
|
669
|
+
`robot_lab-audit`. See
|
|
670
|
+
[Live Narration](../guides/observability.md#live-narration-robotlabnarrator).
|
|
671
|
+
|
|
672
|
+
### enable!
|
|
673
|
+
|
|
674
|
+
```ruby
|
|
675
|
+
RobotLab::Narrator.enable! # narrate to $stderr
|
|
676
|
+
RobotLab::Narrator.enable!(output: $stdout)
|
|
677
|
+
# => the Narrator class, so it chains
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
Sets the output and registers the narrator **globally** via `RobotLab.on(self)`.
|
|
681
|
+
For finer scope, skip `enable!` and register it like any other hook:
|
|
682
|
+
`robot.on(RobotLab::Narrator)` or `network.on(RobotLab::Narrator)` — but set
|
|
683
|
+
`Narrator.output` yourself first if you do not want `$stderr`.
|
|
684
|
+
|
|
685
|
+
### output / output=
|
|
686
|
+
|
|
687
|
+
```ruby
|
|
688
|
+
RobotLab::Narrator.output = $stdout
|
|
689
|
+
RobotLab::Narrator.output # => IO, defaulting to $stderr
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
Narration is written with `IO#puts`, not `Kernel#warn` — `warn` is silenced when
|
|
693
|
+
`$VERBOSE` is `nil`, which is common under `bundle exec`.
|
|
694
|
+
|
|
695
|
+
### MAX
|
|
696
|
+
|
|
697
|
+
```ruby
|
|
698
|
+
RobotLab::Narrator::MAX # => 80
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
Characters before a narrated line is clipped with an ellipsis.
|
|
702
|
+
|
|
703
|
+
### Hook methods
|
|
704
|
+
|
|
705
|
+
| Hook | Output |
|
|
706
|
+
|------|--------|
|
|
707
|
+
| `before_llm_generation(ctx)` | ` · <robot>: thinking…` |
|
|
708
|
+
| `before_tool_call(ctx)` | ` · → <tool_name> <first_arg>="<clipped value>"` |
|
|
709
|
+
| `after_tool_call(ctx)` | ` · ✗ <clipped error message>` — **only when the call raised** |
|
|
710
|
+
|
|
711
|
+
All three rescue `StandardError` and return `nil`, so narration can never break a
|
|
712
|
+
run.
|
|
713
|
+
|
|
714
|
+
---
|
|
715
|
+
|
|
716
|
+
## RobotLab::Config
|
|
717
|
+
|
|
718
|
+
`RobotLab.config` — a `MywayConfig::Base` subclass. For the settings themselves,
|
|
719
|
+
file locations, and precedence rules, see
|
|
720
|
+
[Configuration](../getting-started/configuration.md).
|
|
721
|
+
|
|
722
|
+
| Method | Returns | Description |
|
|
723
|
+
|--------|---------|-------------|
|
|
724
|
+
| `logger` | `Logger` | The configured logger. Defaults to `Rails.logger` under Rails, else `Logger.new($stdout, level: Logger::INFO)` |
|
|
725
|
+
| `logger=` | — | Runtime-only; not read from any config file |
|
|
726
|
+
| `development?` / `test?` / `production?` | `Boolean` | Current-environment predicates, from `MywayConfig::Base` |
|
|
727
|
+
| `after_load` | `void` | Applies the `ruby_llm:` section to RubyLLM and points `prompt_manager` at the resolved template path. `RobotLab.config` calls it once on first construction |
|
|
728
|
+
| `apply_ruby_llm_config!` | `void` | Just the RubyLLM half of `after_load` — provider API keys and endpoints, OpenAI org/project options, default models, connection/retry settings, and logging options. Call it after mutating `config.ruby_llm` at runtime to push the change into RubyLLM |
|
|
729
|
+
|
|
730
|
+
```ruby
|
|
731
|
+
RobotLab.configure { |c| c.ruby_llm.request_timeout = 300 }
|
|
732
|
+
RobotLab.config.apply_ruby_llm_config! # <- otherwise RubyLLM keeps the old value
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
API keys fall back to the standard provider environment variables
|
|
736
|
+
(`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `AWS_ACCESS_KEY_ID`, …) when the
|
|
737
|
+
corresponding config key is unset, so the `ROBOT_LAB_RUBY_LLM__` prefix is
|
|
738
|
+
optional for credentials.
|
|
739
|
+
|
|
740
|
+
---
|
|
741
|
+
|
|
742
|
+
## RobotLab::MCP::ServerDiscovery
|
|
743
|
+
|
|
744
|
+
Narrows a configured MCP server list to the ones relevant to the current message,
|
|
745
|
+
so a robot with many servers connects only what it needs. Activated with
|
|
746
|
+
`mcp_discovery: true` — see [MCP Integration](../guides/mcp-integration.md).
|
|
747
|
+
|
|
748
|
+
### DEFAULT_THRESHOLD
|
|
749
|
+
|
|
750
|
+
```ruby
|
|
751
|
+
RobotLab::MCP::ServerDiscovery::DEFAULT_THRESHOLD # => 0.05
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
Deliberately low: server descriptions are a sentence long, so cosine scores are
|
|
755
|
+
small even for a clearly on-topic query.
|
|
756
|
+
|
|
757
|
+
### select
|
|
758
|
+
|
|
759
|
+
```ruby
|
|
760
|
+
RobotLab::MCP::ServerDiscovery.select(query, from: servers, threshold: 0.05)
|
|
761
|
+
# => Array<Hash, MCP::Server>
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
**Returns `from` unchanged** — connecting everything — whenever discovery cannot
|
|
765
|
+
make a confident call: an empty list, a blank query, no server carrying a
|
|
766
|
+
`description`, no server scoring at or above `threshold`, or the `classifier` gem
|
|
767
|
+
not being installed (`DependencyError` is rescued, not propagated). Discovery can
|
|
768
|
+
therefore only ever *narrow* a run, never break one.
|
|
769
|
+
|
|
770
|
+
### score / topic_text / description_for / any_descriptions?
|
|
771
|
+
|
|
772
|
+
```ruby
|
|
773
|
+
ServerDiscovery.score(query, server) # => Float — tf cosine vs. topic_text
|
|
774
|
+
ServerDiscovery.topic_text(server) # => "github GitHub repos, issues, ..."
|
|
775
|
+
ServerDiscovery.description_for(server) # => String ("" when absent)
|
|
776
|
+
ServerDiscovery.any_descriptions?(servers) # => Boolean
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
All four accept either a config `Hash` (`server[:name]`, `server[:description]`)
|
|
780
|
+
or an `MCP::Server` instance. `topic_text` is `"<name> <description>"` — the name
|
|
781
|
+
participates in matching, so a well-named server scores even with a thin
|
|
782
|
+
description.
|
|
783
|
+
|
|
784
|
+
---
|
|
785
|
+
|
|
786
|
+
## RobotLab::MCP::ConnectionPoller
|
|
787
|
+
|
|
788
|
+
Multiplexes I/O across multiple **stdio** MCP transports with a single
|
|
789
|
+
`IO.select` loop, instead of each client blocking independently behind its own
|
|
790
|
+
`Timeout.timeout`. Async-based transports (SSE, WebSocket, StreamableHTTP) are
|
|
791
|
+
unaffected — they already yield to the fiber scheduler. See
|
|
792
|
+
[Transports](mcp/transports.md).
|
|
793
|
+
|
|
794
|
+
Unlike [`BusPoller`](#robotlabbuspoller), this one **does** own a background
|
|
795
|
+
thread.
|
|
796
|
+
|
|
797
|
+
### POLL_INTERVAL
|
|
798
|
+
|
|
799
|
+
```ruby
|
|
800
|
+
RobotLab::MCP::ConnectionPoller::POLL_INTERVAL # => 0.1 (seconds)
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
### Methods
|
|
804
|
+
|
|
805
|
+
| Method | Returns | Description |
|
|
806
|
+
|--------|---------|-------------|
|
|
807
|
+
| `start` | `self` | Spawn the multiplexing thread (named `RobotLab::MCP::ConnectionPoller`). Idempotent |
|
|
808
|
+
| `stop(timeout: 5)` | `self` | Stop the thread and cancel every pending request with an `MCPError`. Idempotent |
|
|
809
|
+
| `running?` | `Boolean` | Whether the thread is live |
|
|
810
|
+
| `register(client)` | — | Add the client's `transport.stdout` to the select set. **Silently ignores a non-stdio client** |
|
|
811
|
+
| `unregister(client)` | — | Remove it; likewise a no-op for non-stdio clients |
|
|
812
|
+
| `send_request(client, message, timeout:)` | response | Write `message.to_json` to the client's stdin and block on that client's queue for the matching response. `timeout:` is required |
|
|
813
|
+
|
|
814
|
+
```ruby
|
|
815
|
+
poller = RobotLab::MCP::ConnectionPoller.new.start
|
|
816
|
+
client = RobotLab::MCP::Client.new(server_config, poller: poller)
|
|
817
|
+
client.connect
|
|
818
|
+
# ...
|
|
819
|
+
poller.stop
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
---
|
|
823
|
+
|
|
824
|
+
## RobotLab::Streaming::SequenceCounter
|
|
825
|
+
|
|
826
|
+
Thread-safe monotonic counter for event ordering.
|
|
827
|
+
|
|
828
|
+
| Method | Returns | Description |
|
|
829
|
+
|--------|---------|-------------|
|
|
830
|
+
| `SequenceCounter.new(start: 0)` | `SequenceCounter` | |
|
|
831
|
+
| `next` | `Integer` | Increment and return the new value |
|
|
832
|
+
| `current` | `Integer` | Read without incrementing |
|
|
833
|
+
| `reset(value = 0)` | `Integer` | Set the counter |
|
|
834
|
+
|
|
835
|
+
!!! warning "Nothing in the framework uses this"
|
|
836
|
+
Its only consumer is `Streaming::Context`, which core itself never
|
|
837
|
+
constructs. See [Streaming](streaming/index.md) for what actually streams.
|
|
838
|
+
|
|
839
|
+
---
|
|
840
|
+
|
|
841
|
+
## See Also
|
|
842
|
+
|
|
843
|
+
- [Core Classes](core/index.md) — `Robot`, `Network`, `Memory`, `Tool`, `RobotResult`
|
|
844
|
+
- [Hooks API](hooks.md) — the extension seam these classes are wired into
|
|
845
|
+
- [Skills API](skills.md) — `AgentSkill`, `Capabilities`, `ScriptTool`, `Sandbox`
|
|
846
|
+
- [Errors](errors.md) — including `Errors.retryable?` and `Errors.retryable_classes`
|