robot_lab 0.0.1 → 0.0.4
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/.github/workflows/deploy-github-pages.yml +9 -9
- data/.irbrc +6 -0
- data/CHANGELOG.md +90 -0
- data/README.md +203 -46
- data/Rakefile +70 -1
- data/docs/api/core/index.md +12 -0
- data/docs/api/core/robot.md +478 -130
- data/docs/api/core/tool.md +205 -209
- data/docs/api/history/active-record-adapter.md +174 -94
- data/docs/api/history/config.md +186 -93
- data/docs/api/history/index.md +57 -61
- data/docs/api/history/thread-manager.md +123 -73
- data/docs/api/mcp/client.md +119 -48
- data/docs/api/mcp/index.md +75 -60
- data/docs/api/mcp/server.md +120 -136
- data/docs/api/mcp/transports.md +172 -184
- data/docs/api/streaming/context.md +157 -74
- data/docs/api/streaming/events.md +114 -166
- data/docs/api/streaming/index.md +74 -72
- data/docs/architecture/core-concepts.md +361 -112
- data/docs/architecture/index.md +97 -59
- data/docs/architecture/message-flow.md +138 -129
- data/docs/architecture/network-orchestration.md +197 -50
- data/docs/architecture/robot-execution.md +199 -146
- data/docs/architecture/state-management.md +255 -187
- data/docs/concepts.md +312 -48
- data/docs/examples/basic-chat.md +89 -77
- data/docs/examples/index.md +222 -47
- data/docs/examples/mcp-server.md +207 -203
- data/docs/examples/multi-robot-network.md +129 -35
- data/docs/examples/rails-application.md +159 -160
- data/docs/examples/tool-usage.md +295 -204
- data/docs/getting-started/configuration.md +275 -162
- data/docs/getting-started/index.md +1 -1
- data/docs/getting-started/installation.md +22 -13
- data/docs/getting-started/quick-start.md +166 -121
- data/docs/guides/building-robots.md +417 -212
- data/docs/guides/creating-networks.md +94 -24
- data/docs/guides/mcp-integration.md +152 -113
- data/docs/guides/memory.md +220 -164
- data/docs/guides/streaming.md +80 -110
- data/docs/guides/using-tools.md +259 -212
- data/docs/index.md +50 -37
- data/examples/01_simple_robot.rb +6 -9
- data/examples/02_tools.rb +6 -9
- data/examples/03_network.rb +13 -14
- data/examples/04_mcp.rb +5 -8
- data/examples/05_streaming.rb +5 -8
- data/examples/06_prompt_templates.rb +42 -37
- data/examples/07_network_memory.rb +13 -14
- data/examples/08_llm_config.rb +140 -0
- data/examples/09_chaining.rb +223 -0
- data/examples/10_memory.rb +331 -0
- data/examples/11_network_introspection.rb +230 -0
- data/examples/12_message_bus.rb +74 -0
- data/examples/13_spawn.rb +90 -0
- data/examples/14_rusty_circuit/comic.rb +143 -0
- data/examples/14_rusty_circuit/display.rb +203 -0
- data/examples/14_rusty_circuit/heckler.rb +57 -0
- data/examples/14_rusty_circuit/open_mic.rb +121 -0
- data/examples/14_rusty_circuit/prompts/open_mic_comic.md +20 -0
- data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +23 -0
- data/examples/14_rusty_circuit/prompts/open_mic_scout.md +20 -0
- data/examples/14_rusty_circuit/scout.rb +173 -0
- data/examples/14_rusty_circuit/scout_notes.md +89 -0
- data/examples/14_rusty_circuit/show.log +234 -0
- data/examples/15_memory_network_and_bus/editor_in_chief.rb +24 -0
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +206 -0
- data/examples/15_memory_network_and_bus/linux_writer.rb +80 -0
- data/examples/15_memory_network_and_bus/os_editor.rb +46 -0
- data/examples/15_memory_network_and_bus/os_writer.rb +46 -0
- data/examples/15_memory_network_and_bus/output/combined_article.md +13 -0
- data/examples/15_memory_network_and_bus/output/final_article.md +15 -0
- data/examples/15_memory_network_and_bus/output/linux_draft.md +5 -0
- data/examples/15_memory_network_and_bus/output/mac_draft.md +7 -0
- data/examples/15_memory_network_and_bus/output/memory.json +13 -0
- data/examples/15_memory_network_and_bus/output/revision_1.md +19 -0
- data/examples/15_memory_network_and_bus/output/revision_2.md +15 -0
- data/examples/15_memory_network_and_bus/output/windows_draft.md +7 -0
- data/examples/15_memory_network_and_bus/prompts/os_advocate.md +13 -0
- data/examples/15_memory_network_and_bus/prompts/os_chief.md +13 -0
- data/examples/15_memory_network_and_bus/prompts/os_editor.md +13 -0
- data/examples/README.md +197 -0
- data/examples/prompts/{assistant/system.txt.erb → assistant.md} +3 -0
- data/examples/prompts/{billing/system.txt.erb → billing.md} +3 -0
- data/examples/prompts/{classifier/system.txt.erb → classifier.md} +3 -0
- data/examples/prompts/comedian.md +6 -0
- data/examples/prompts/comedy_critic.md +10 -0
- data/examples/prompts/configurable.md +9 -0
- data/examples/prompts/dispatcher.md +12 -0
- data/examples/prompts/{entity_extractor/system.txt.erb → entity_extractor.md} +3 -0
- data/examples/prompts/{escalation/system.txt.erb → escalation.md} +7 -0
- data/examples/prompts/frontmatter_mcp_test.md +9 -0
- data/examples/prompts/frontmatter_named_test.md +5 -0
- data/examples/prompts/frontmatter_tools_test.md +6 -0
- data/examples/prompts/{general/system.txt.erb → general.md} +3 -0
- data/examples/prompts/{github_assistant/system.txt.erb → github_assistant.md} +8 -0
- data/examples/prompts/{helper/system.txt.erb → helper.md} +3 -0
- data/examples/prompts/{keyword_extractor/system.txt.erb → keyword_extractor.md} +3 -0
- data/examples/prompts/llm_config_demo.md +20 -0
- data/examples/prompts/{order_support/system.txt.erb → order_support.md} +8 -0
- data/examples/prompts/os_advocate.md +13 -0
- data/examples/prompts/os_chief.md +13 -0
- data/examples/prompts/os_editor.md +13 -0
- data/examples/prompts/{product_support/system.txt.erb → product_support.md} +7 -0
- data/examples/prompts/{sentiment_analyzer/system.txt.erb → sentiment_analyzer.md} +3 -0
- data/examples/prompts/{synthesizer/system.txt.erb → synthesizer.md} +3 -0
- data/examples/prompts/{technical/system.txt.erb → technical.md} +3 -0
- data/examples/prompts/{triage/system.txt.erb → triage.md} +6 -0
- data/lib/generators/robot_lab/templates/initializer.rb.tt +1 -1
- data/lib/robot_lab/adapters/openai.rb +2 -1
- data/lib/robot_lab/ask_user.rb +75 -0
- data/lib/robot_lab/config/defaults.yml +121 -0
- data/lib/robot_lab/config.rb +183 -0
- data/lib/robot_lab/error.rb +6 -0
- data/lib/robot_lab/mcp/client.rb +1 -1
- data/lib/robot_lab/memory.rb +2 -2
- data/lib/robot_lab/robot.rb +523 -249
- data/lib/robot_lab/robot_message.rb +44 -0
- data/lib/robot_lab/robot_result.rb +1 -0
- data/lib/robot_lab/robotic_model.rb +1 -1
- data/lib/robot_lab/streaming/context.rb +1 -1
- data/lib/robot_lab/tool.rb +108 -172
- data/lib/robot_lab/tool_config.rb +1 -1
- data/lib/robot_lab/tool_manifest.rb +2 -18
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +66 -55
- metadata +107 -116
- data/examples/prompts/assistant/user.txt.erb +0 -1
- data/examples/prompts/billing/user.txt.erb +0 -1
- data/examples/prompts/classifier/user.txt.erb +0 -1
- data/examples/prompts/entity_extractor/user.txt.erb +0 -3
- data/examples/prompts/escalation/user.txt.erb +0 -34
- data/examples/prompts/general/user.txt.erb +0 -1
- data/examples/prompts/github_assistant/user.txt.erb +0 -1
- data/examples/prompts/helper/user.txt.erb +0 -1
- data/examples/prompts/keyword_extractor/user.txt.erb +0 -3
- data/examples/prompts/order_support/user.txt.erb +0 -22
- data/examples/prompts/product_support/user.txt.erb +0 -32
- data/examples/prompts/sentiment_analyzer/user.txt.erb +0 -3
- data/examples/prompts/synthesizer/user.txt.erb +0 -15
- data/examples/prompts/technical/user.txt.erb +0 -1
- data/examples/prompts/triage/user.txt.erb +0 -17
- data/lib/robot_lab/configuration.rb +0 -143
|
@@ -4,87 +4,159 @@ This page provides an in-depth look at RobotLab's fundamental building blocks.
|
|
|
4
4
|
|
|
5
5
|
## Robot
|
|
6
6
|
|
|
7
|
-
A Robot is the primary unit of computation in RobotLab. It wraps
|
|
7
|
+
A Robot is the primary unit of computation in RobotLab. It is a subclass of `RubyLLM::Agent` that wraps a persistent `@chat` with:
|
|
8
8
|
|
|
9
9
|
- A unique identity (name, description)
|
|
10
|
-
- A personality (system prompt/template)
|
|
10
|
+
- A personality (system prompt and/or template)
|
|
11
11
|
- Capabilities (tools, MCP connections)
|
|
12
|
-
-
|
|
12
|
+
- Model and inference configuration
|
|
13
|
+
- Inherent memory (key-value store)
|
|
13
14
|
|
|
14
15
|
### Robot Anatomy
|
|
15
16
|
|
|
16
17
|
```ruby
|
|
17
|
-
robot = RobotLab.build
|
|
18
|
-
name "support_agent" # Unique identifier
|
|
19
|
-
description "Handles support requests" # Used for routing hints
|
|
20
|
-
model "claude-sonnet-4" # LLM model
|
|
21
|
-
|
|
22
|
-
# System prompt - defines personality
|
|
23
|
-
template <<~PROMPT
|
|
18
|
+
robot = RobotLab.build(
|
|
19
|
+
name: "support_agent", # Unique identifier
|
|
20
|
+
description: "Handles support requests", # Used for routing hints
|
|
21
|
+
model: "claude-sonnet-4", # LLM model
|
|
22
|
+
system_prompt: <<~PROMPT, # Inline system prompt
|
|
24
23
|
You are a friendly customer support agent for Acme Corp.
|
|
25
24
|
Always be polite and helpful. If you don't know something,
|
|
26
25
|
say so honestly.
|
|
27
26
|
PROMPT
|
|
27
|
+
local_tools: [OrderLookup, RefundProcessor], # RubyLLM::Tool subclasses
|
|
28
|
+
mcp: :inherit, # Use network's MCP servers
|
|
29
|
+
temperature: 0.7 # Inference parameter
|
|
30
|
+
)
|
|
31
|
+
```
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
tool :lookup_account do
|
|
31
|
-
description "Look up customer account"
|
|
32
|
-
parameter :email, type: :string, required: true
|
|
33
|
-
handler { |email:, **_| Account.find_by_email(email)&.to_h }
|
|
34
|
-
end
|
|
33
|
+
Or with a template:
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
```ruby
|
|
36
|
+
robot = RobotLab.build(
|
|
37
|
+
name: "support_agent",
|
|
38
|
+
template: :support, # Loads prompts/support.md
|
|
39
|
+
context: { company: "Acme Corp" }, # Template variables
|
|
40
|
+
local_tools: [OrderLookup]
|
|
41
|
+
)
|
|
39
42
|
```
|
|
40
43
|
|
|
41
44
|
### Robot Lifecycle
|
|
42
45
|
|
|
43
46
|
```mermaid
|
|
44
47
|
stateDiagram-v2
|
|
45
|
-
[*] --> Created: RobotLab.build
|
|
46
|
-
Created --> Running: robot.run(
|
|
47
|
-
Running -->
|
|
48
|
-
|
|
49
|
-
Running --> Completed:
|
|
50
|
-
Completed -->
|
|
48
|
+
[*] --> Created: RobotLab.build / Robot.new
|
|
49
|
+
Created --> Running: robot.run("message")
|
|
50
|
+
Running --> ToolLoop: tool_call from LLM
|
|
51
|
+
ToolLoop --> Running: tool result sent back
|
|
52
|
+
Running --> Completed: final text response
|
|
53
|
+
Completed --> Running: robot.run("next message")
|
|
54
|
+
Completed --> [*]: robot.disconnect
|
|
51
55
|
```
|
|
52
56
|
|
|
57
|
+
The persistent `@chat` maintains conversation history across multiple `run` calls, making the robot stateful.
|
|
58
|
+
|
|
53
59
|
### Robot Properties
|
|
54
60
|
|
|
55
61
|
| Property | Type | Description |
|
|
56
62
|
|----------|------|-------------|
|
|
57
|
-
| `name` | String | Unique identifier within network |
|
|
58
|
-
| `description` | String | What the robot does |
|
|
59
|
-
| `model` | String | LLM model
|
|
60
|
-
| `template` |
|
|
61
|
-
| `
|
|
62
|
-
| `
|
|
63
|
+
| `name` | `String` | Unique identifier within network |
|
|
64
|
+
| `description` | `String`, `nil` | What the robot does |
|
|
65
|
+
| `model` | `String` | LLM model ID (resolved from chat) |
|
|
66
|
+
| `template` | `Symbol`, `nil` | Prompt template identifier |
|
|
67
|
+
| `system_prompt` | `String`, `nil` | Inline system prompt |
|
|
68
|
+
| `local_tools` | `Array` | Locally defined tools |
|
|
69
|
+
| `mcp_clients` | `Hash` | Connected MCP clients by server name |
|
|
70
|
+
| `mcp_tools` | `Array` | Tools discovered from MCP servers |
|
|
71
|
+
| `memory` | `Memory` | Inherent key-value memory |
|
|
72
|
+
| `bus` | `TypedBus::MessageBus`, `nil` | Message bus instance |
|
|
73
|
+
| `outbox` | `Hash` | Sent messages tracked with status and replies |
|
|
74
|
+
| `mcp_config` | `Symbol`, `Array` | Build-time MCP configuration |
|
|
75
|
+
| `tools_config` | `Symbol`, `Array` | Build-time tools configuration |
|
|
76
|
+
|
|
77
|
+
### Running a Robot
|
|
78
|
+
|
|
79
|
+
The primary method is `robot.run("message")`:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
result = robot.run("What is the weather in Berlin?")
|
|
83
|
+
puts result.last_text_content
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
With runtime overrides:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
result = robot.run("Analyze this",
|
|
90
|
+
memory: { data: report },
|
|
91
|
+
mcp: :none,
|
|
92
|
+
tools: :none
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
With streaming:
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
robot.run("Tell me a story") do |event|
|
|
100
|
+
print event.text if event.respond_to?(:text)
|
|
101
|
+
end
|
|
102
|
+
```
|
|
63
103
|
|
|
64
104
|
## Tool
|
|
65
105
|
|
|
66
|
-
Tools give robots the ability to interact with external systems.
|
|
106
|
+
Tools give robots the ability to interact with external systems. There are two patterns for defining tools.
|
|
67
107
|
|
|
68
|
-
### Tool
|
|
108
|
+
### RubyLLM::Tool Subclass (Primary)
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
class GetWeather < RubyLLM::Tool
|
|
112
|
+
description "Get current weather for a location"
|
|
113
|
+
|
|
114
|
+
param :location, type: "string", desc: "City name"
|
|
115
|
+
param :unit, type: "string", desc: "celsius or fahrenheit"
|
|
116
|
+
|
|
117
|
+
def execute(location:, unit: "celsius")
|
|
118
|
+
WeatherAPI.current(location, unit: unit)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Tools defined as `RubyLLM::Tool` subclasses are passed to robots via `local_tools:`:
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
robot = RobotLab.build(
|
|
127
|
+
name: "weather_bot",
|
|
128
|
+
system_prompt: "You provide weather information.",
|
|
129
|
+
local_tools: [GetWeather]
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### RobotLab::Tool Inline
|
|
134
|
+
|
|
135
|
+
For simpler tools that do not need a class:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
tool = RobotLab::Tool.new(
|
|
139
|
+
name: "get_time",
|
|
140
|
+
description: "Get the current time",
|
|
141
|
+
handler: ->(_input, **_opts) { Time.now.to_s }
|
|
142
|
+
)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
With parameter schema:
|
|
69
146
|
|
|
70
147
|
```ruby
|
|
71
148
|
tool = RobotLab::Tool.new(
|
|
72
149
|
name: "get_weather",
|
|
73
|
-
description: "Get
|
|
150
|
+
description: "Get weather for a location",
|
|
74
151
|
parameters: {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
description: "City name"
|
|
78
|
-
required: true
|
|
152
|
+
type: "object",
|
|
153
|
+
properties: {
|
|
154
|
+
location: { type: "string", description: "City name" }
|
|
79
155
|
},
|
|
80
|
-
|
|
81
|
-
type: "string",
|
|
82
|
-
enum: ["celsius", "fahrenheit"],
|
|
83
|
-
default: "celsius"
|
|
84
|
-
}
|
|
156
|
+
required: ["location"]
|
|
85
157
|
},
|
|
86
|
-
handler: ->(
|
|
87
|
-
WeatherAPI.current(location
|
|
158
|
+
handler: ->(input, **_opts) {
|
|
159
|
+
WeatherAPI.current(input[:location])
|
|
88
160
|
}
|
|
89
161
|
)
|
|
90
162
|
```
|
|
@@ -93,40 +165,79 @@ tool = RobotLab::Tool.new(
|
|
|
93
165
|
|
|
94
166
|
When an LLM decides to use a tool:
|
|
95
167
|
|
|
96
|
-
1. LLM generates a
|
|
97
|
-
2.
|
|
98
|
-
3. Tool
|
|
99
|
-
4.
|
|
168
|
+
1. LLM generates a tool call with tool name and arguments
|
|
169
|
+
2. `@chat` (RubyLLM) identifies the tool from its registered tools
|
|
170
|
+
3. For `RubyLLM::Tool`: calls the `execute` method with keyword arguments
|
|
171
|
+
4. For `RobotLab::Tool`: calls the `call` method with input hash and context
|
|
100
172
|
5. Result is sent back to the LLM for continued processing
|
|
173
|
+
6. Loop repeats until the LLM produces a final text response
|
|
174
|
+
|
|
175
|
+
### Error Handling
|
|
176
|
+
|
|
177
|
+
Tool errors are captured and returned to the LLM:
|
|
101
178
|
|
|
102
|
-
|
|
179
|
+
```ruby
|
|
180
|
+
def execute(order_id:)
|
|
181
|
+
order = ORDERS[order_id]
|
|
182
|
+
if order
|
|
183
|
+
order
|
|
184
|
+
else
|
|
185
|
+
{ error: "Order not found" }
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
```
|
|
103
189
|
|
|
104
|
-
|
|
190
|
+
## Memory
|
|
191
|
+
|
|
192
|
+
Memory is a reactive key-value store used by robots and networks.
|
|
193
|
+
|
|
194
|
+
### Standalone vs Network Memory
|
|
195
|
+
|
|
196
|
+
- **Standalone**: Each robot has its own inherent `Memory` instance (`robot.memory`)
|
|
197
|
+
- **In a Network**: All robots share the network's `Memory` instance
|
|
105
198
|
|
|
106
199
|
```ruby
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
#
|
|
110
|
-
|
|
111
|
-
|
|
200
|
+
# Standalone memory
|
|
201
|
+
robot.memory[:user_id] = 123
|
|
202
|
+
robot.memory[:user_id] # => 123
|
|
203
|
+
|
|
204
|
+
# Network memory is passed automatically
|
|
205
|
+
network = RobotLab.create_network(name: "pipeline") do
|
|
206
|
+
task :robot_a, robot_a, depends_on: :none
|
|
207
|
+
task :robot_b, robot_b, depends_on: [:robot_a]
|
|
208
|
+
end
|
|
209
|
+
# Both robot_a and robot_b share network.memory during execution
|
|
112
210
|
```
|
|
113
211
|
|
|
114
|
-
|
|
115
|
-
Use `**_context` to accept but ignore context parameters:
|
|
116
|
-
```ruby
|
|
117
|
-
handler: ->(location:, **_context) { ... }
|
|
118
|
-
```
|
|
212
|
+
### Reserved Keys
|
|
119
213
|
|
|
120
|
-
|
|
214
|
+
| Key | Type | Description |
|
|
215
|
+
|-----|------|-------------|
|
|
216
|
+
| `:data` | `Hash` | Runtime data (accessible via `memory.data.key_name`) |
|
|
217
|
+
| `:results` | `Array` | Accumulated robot results |
|
|
218
|
+
| `:messages` | `Array` | Conversation history |
|
|
219
|
+
| `:session_id` | `String` | Session identifier |
|
|
220
|
+
| `:cache` | `Module` | Semantic cache (RubyLLM::SemanticCache) |
|
|
121
221
|
|
|
122
|
-
|
|
222
|
+
### Reactive Features
|
|
223
|
+
|
|
224
|
+
Memory supports pub/sub semantics for inter-robot communication:
|
|
123
225
|
|
|
124
226
|
```ruby
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
)
|
|
227
|
+
# Write a value (notifies subscribers, wakes waiters)
|
|
228
|
+
memory.set(:sentiment, { score: 0.8 })
|
|
229
|
+
|
|
230
|
+
# Read a value (non-blocking)
|
|
231
|
+
memory.get(:sentiment) # => { score: 0.8 } or nil
|
|
232
|
+
|
|
233
|
+
# Blocking read (waits until value exists)
|
|
234
|
+
memory.get(:sentiment, wait: true) # Blocks indefinitely
|
|
235
|
+
memory.get(:sentiment, wait: 30) # Blocks up to 30 seconds
|
|
236
|
+
|
|
237
|
+
# Subscribe to changes
|
|
238
|
+
memory.subscribe(:sentiment) do |change|
|
|
239
|
+
puts "#{change.key} = #{change.value} (written by #{change.writer})"
|
|
240
|
+
end
|
|
130
241
|
```
|
|
131
242
|
|
|
132
243
|
## Message Types
|
|
@@ -136,27 +247,22 @@ RobotLab uses a type hierarchy for messages:
|
|
|
136
247
|
```mermaid
|
|
137
248
|
classDiagram
|
|
138
249
|
Message <|-- TextMessage
|
|
139
|
-
Message <|--
|
|
140
|
-
|
|
141
|
-
ToolMessage <|-- ToolResultMessage
|
|
250
|
+
Message <|-- ToolCallMessage
|
|
251
|
+
Message <|-- ToolResultMessage
|
|
142
252
|
|
|
143
253
|
class Message {
|
|
144
254
|
+String type
|
|
145
255
|
+String role
|
|
146
|
-
+
|
|
256
|
+
+content
|
|
147
257
|
+String stop_reason
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
class TextMessage {
|
|
151
258
|
+text?()
|
|
152
|
-
+
|
|
153
|
-
+
|
|
259
|
+
+tool_call?()
|
|
260
|
+
+tool_result?()
|
|
261
|
+
+stopped?()
|
|
154
262
|
}
|
|
155
263
|
|
|
156
|
-
class
|
|
157
|
-
+String
|
|
158
|
-
+String name
|
|
159
|
-
+Hash input
|
|
264
|
+
class TextMessage {
|
|
265
|
+
+String content
|
|
160
266
|
}
|
|
161
267
|
|
|
162
268
|
class ToolCallMessage {
|
|
@@ -166,6 +272,8 @@ classDiagram
|
|
|
166
272
|
class ToolResultMessage {
|
|
167
273
|
+ToolMessage tool
|
|
168
274
|
+Hash content
|
|
275
|
+
+success?()
|
|
276
|
+
+error?()
|
|
169
277
|
}
|
|
170
278
|
```
|
|
171
279
|
|
|
@@ -176,7 +284,7 @@ classDiagram
|
|
|
176
284
|
| `user` | Input from the user |
|
|
177
285
|
| `assistant` | Response from the LLM |
|
|
178
286
|
| `system` | System instructions |
|
|
179
|
-
| `
|
|
287
|
+
| `tool_result` | Tool execution result |
|
|
180
288
|
|
|
181
289
|
### Stop Reasons
|
|
182
290
|
|
|
@@ -184,60 +292,201 @@ classDiagram
|
|
|
184
292
|
|--------|-------------|
|
|
185
293
|
| `stop` | Natural completion |
|
|
186
294
|
| `tool` | Tool call requested |
|
|
187
|
-
| `max_tokens` | Token limit reached |
|
|
188
295
|
|
|
189
296
|
## RobotResult
|
|
190
297
|
|
|
191
298
|
The output from a robot execution:
|
|
192
299
|
|
|
193
300
|
```ruby
|
|
194
|
-
result = robot.run(
|
|
195
|
-
|
|
196
|
-
result.robot_name
|
|
197
|
-
result.output
|
|
198
|
-
result.tool_calls
|
|
199
|
-
result.stop_reason
|
|
200
|
-
result.created_at
|
|
301
|
+
result = robot.run("Hello!")
|
|
302
|
+
|
|
303
|
+
result.robot_name # => "support_agent"
|
|
304
|
+
result.output # => [TextMessage, ...]
|
|
305
|
+
result.tool_calls # => [ToolResultMessage, ...]
|
|
306
|
+
result.stop_reason # => "stop"
|
|
307
|
+
result.created_at # => Time
|
|
308
|
+
result.id # => UUID string
|
|
201
309
|
```
|
|
202
310
|
|
|
203
311
|
### Accessing Response Content
|
|
204
312
|
|
|
205
313
|
```ruby
|
|
206
|
-
# Get text response
|
|
207
|
-
text = result.
|
|
314
|
+
# Get last text response (most common)
|
|
315
|
+
text = result.last_text_content
|
|
208
316
|
|
|
209
317
|
# Check if tools were called
|
|
210
|
-
has_tools = result.
|
|
318
|
+
has_tools = result.has_tool_calls?
|
|
319
|
+
|
|
320
|
+
# Check if execution completed naturally
|
|
321
|
+
result.stopped?
|
|
322
|
+
|
|
323
|
+
# Serialization
|
|
324
|
+
result.export # => Hash (excludes debug fields)
|
|
325
|
+
result.to_h # => Hash (includes debug fields)
|
|
326
|
+
result.to_json # => JSON string
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Configuration
|
|
330
|
+
|
|
331
|
+
RobotLab uses `MywayConfig` for configuration. There is no `RobotLab.configure` block. Configuration is loaded from:
|
|
332
|
+
|
|
333
|
+
1. Bundled defaults (`lib/robot_lab/config/defaults.yml`)
|
|
334
|
+
2. Environment-specific overrides
|
|
335
|
+
3. XDG config files (`~/.config/robot_lab/config.yml`)
|
|
336
|
+
4. Project config (`./config/robot_lab.yml`)
|
|
337
|
+
5. Environment variables (`ROBOT_LAB_*` prefix)
|
|
211
338
|
|
|
212
|
-
|
|
213
|
-
|
|
339
|
+
Access via `RobotLab.config`:
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
RobotLab.config.ruby_llm.model # => "claude-sonnet-4"
|
|
343
|
+
RobotLab.config.ruby_llm.request_timeout # => 120
|
|
214
344
|
```
|
|
215
345
|
|
|
216
346
|
## Configuration Hierarchy
|
|
217
347
|
|
|
218
|
-
|
|
348
|
+
Tools and MCP servers use a cascading configuration system:
|
|
219
349
|
|
|
220
350
|
```
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
351
|
+
RobotLab.config (global)
|
|
352
|
+
|
|
|
353
|
+
+-- mcp: [server1, server2]
|
|
354
|
+
+-- tools: [tool1, tool2]
|
|
355
|
+
|
|
|
356
|
+
+-- Network
|
|
357
|
+
| |
|
|
358
|
+
| +-- mcp: :inherit | :none | [servers]
|
|
359
|
+
| +-- tools: :inherit | :none | [tools]
|
|
360
|
+
| |
|
|
361
|
+
| +-- Task (per-step config)
|
|
362
|
+
| | +-- context: { department: "billing" }
|
|
363
|
+
| | +-- mcp: :none | :inherit | [servers]
|
|
364
|
+
| | +-- tools: :none | :inherit | [tools]
|
|
365
|
+
| |
|
|
366
|
+
| +-- Robot (build-time config)
|
|
367
|
+
| |
|
|
368
|
+
| +-- mcp: :inherit | :none | [servers]
|
|
369
|
+
| +-- tools: :inherit | :none | [tools]
|
|
370
|
+
| |
|
|
371
|
+
| +-- run() call (runtime config)
|
|
372
|
+
| +-- mcp: :none | [servers]
|
|
373
|
+
| +-- tools: :none | [tools]
|
|
235
374
|
```
|
|
236
375
|
|
|
237
|
-
|
|
376
|
+
Resolution order: **runtime > robot build-time > task > network > global config**.
|
|
377
|
+
|
|
378
|
+
The `:inherit` value pulls from the parent level. `:none` explicitly disables.
|
|
379
|
+
|
|
380
|
+
## Message Bus
|
|
381
|
+
|
|
382
|
+
The **Message Bus** provides bidirectional, cyclic communication between robots, independent of the Network pipeline. While Networks enforce DAG-based (acyclic) execution, the bus enables negotiation loops, convergence patterns, and multi-turn dialogues.
|
|
383
|
+
|
|
384
|
+
### How It Works
|
|
385
|
+
|
|
386
|
+
Robots connect to a shared `TypedBus::MessageBus` via the `bus:` parameter. Each robot gets a typed channel (accepting only `RobotMessage` objects) named after its `name`. Messages are delivered asynchronously via the `async` gem's fiber scheduler.
|
|
387
|
+
|
|
388
|
+
```ruby
|
|
389
|
+
bus = TypedBus::MessageBus.new
|
|
390
|
+
|
|
391
|
+
bob = RobotLab.build(name: "bob", system_prompt: "You tell jokes.", bus: bus)
|
|
392
|
+
alice = RobotLab.build(name: "alice", system_prompt: "You evaluate jokes.", bus: bus)
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### RobotMessage
|
|
396
|
+
|
|
397
|
+
`RobotMessage` is an immutable `Data.define` value object used as the typed envelope:
|
|
398
|
+
|
|
399
|
+
| Field | Type | Description |
|
|
400
|
+
|-------|------|-------------|
|
|
401
|
+
| `id` | `Integer` | Per-robot sequential counter |
|
|
402
|
+
| `from` | `String` | Sender's robot name (= channel name) |
|
|
403
|
+
| `content` | `String`, `Hash` | Message payload |
|
|
404
|
+
| `in_reply_to` | `String`, `nil` | Composite key of the original message (e.g., `"alice:1"`) |
|
|
405
|
+
|
|
406
|
+
Methods: `key` returns `"from:id"` composite identity; `reply?` returns true when `in_reply_to` is set.
|
|
407
|
+
|
|
408
|
+
### Sending and Receiving
|
|
409
|
+
|
|
410
|
+
```ruby
|
|
411
|
+
# Send a message to another robot
|
|
412
|
+
alice.send_message(to: :bob, content: "Tell me a joke.")
|
|
413
|
+
|
|
414
|
+
# Handle incoming messages with auto-ack (1 arg)
|
|
415
|
+
bob.on_message do |message|
|
|
416
|
+
joke = bob.run(message.content.to_s).last_text_content
|
|
417
|
+
bob.reply(message, joke)
|
|
418
|
+
end
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Block arity controls delivery handling: 1 argument auto-acks; 2 arguments give manual control over `delivery.ack!`/`delivery.nack!`.
|
|
422
|
+
|
|
423
|
+
### Dynamic Spawning
|
|
424
|
+
|
|
425
|
+
Robots can create new robots at runtime using `spawn`. The bus is created lazily — no upfront wiring required:
|
|
426
|
+
|
|
427
|
+
```ruby
|
|
428
|
+
dispatcher = RobotLab.build(name: "dispatcher", system_prompt: "You delegate work.")
|
|
429
|
+
|
|
430
|
+
# spawn creates a child on the same bus (bus created automatically)
|
|
431
|
+
helper = dispatcher.spawn(name: "helper", system_prompt: "You answer questions.")
|
|
432
|
+
|
|
433
|
+
# The child can immediately communicate with the parent
|
|
434
|
+
answer = helper.run("What is 2+2?").last_text_content
|
|
435
|
+
helper.send_message(to: :dispatcher, content: answer)
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
Robots can also join a bus after creation using `with_bus`:
|
|
439
|
+
|
|
440
|
+
```ruby
|
|
441
|
+
bot = RobotLab.build(name: "late_joiner", system_prompt: "Hello.")
|
|
442
|
+
bot.with_bus(existing_bus) # now connected to the bus
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**Fan-out messaging**: Multiple robots with the same name all subscribe to the same channel. Messages sent to that name are delivered to all subscribers:
|
|
446
|
+
|
|
447
|
+
```ruby
|
|
448
|
+
worker1 = dispatcher.spawn(name: "worker", system_prompt: "Worker 1")
|
|
449
|
+
worker2 = dispatcher.spawn(name: "worker", system_prompt: "Worker 2")
|
|
450
|
+
dispatcher.send_message(to: :worker, content: "Do this task")
|
|
451
|
+
# Both worker1 and worker2 receive the message
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Bus vs Network
|
|
455
|
+
|
|
456
|
+
| Feature | Network | Message Bus |
|
|
457
|
+
|---------|---------|-------------|
|
|
458
|
+
| Execution model | DAG (acyclic) | Cyclic, bidirectional |
|
|
459
|
+
| Communication | Sequential pipeline | Pub/sub channels |
|
|
460
|
+
| Memory | Shared network memory | Independent per-robot |
|
|
461
|
+
| Use case | Linear workflows | Negotiation, convergence |
|
|
462
|
+
|
|
463
|
+
The bus is purely additive — robots without `bus:` work exactly as before.
|
|
464
|
+
|
|
465
|
+
## Network
|
|
466
|
+
|
|
467
|
+
A Network orchestrates multiple robots in a pipeline workflow using SimpleFlow:
|
|
468
|
+
|
|
469
|
+
```ruby
|
|
470
|
+
network = RobotLab.create_network(name: "support") do
|
|
471
|
+
task :classifier, classifier_robot, depends_on: :none
|
|
472
|
+
task :billing, billing_robot, depends_on: :optional
|
|
473
|
+
task :technical, technical_robot, depends_on: :optional
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
result = network.run(message: "I need help with billing")
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
Networks provide:
|
|
480
|
+
|
|
481
|
+
- **DAG-based execution** via SimpleFlow with `depends_on:` for sequencing
|
|
482
|
+
- **Parallel execution** for tasks with the same dependencies
|
|
483
|
+
- **Optional tasks** activated dynamically by classifier robots
|
|
484
|
+
- **Shared memory** for inter-robot communication
|
|
485
|
+
- **Per-task configuration** via the `Task` wrapper
|
|
486
|
+
- **Broadcast messaging** for network-wide announcements
|
|
238
487
|
|
|
239
488
|
## Next Steps
|
|
240
489
|
|
|
241
490
|
- [Robot Execution](robot-execution.md) - Detailed execution flow
|
|
242
491
|
- [Network Orchestration](network-orchestration.md) - Multi-robot coordination
|
|
243
|
-
- [
|
|
492
|
+
- [Using Tools](../guides/using-tools.md) - Creating and using tools
|