robot_lab 0.0.1 → 0.0.6
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 +140 -0
- data/README.md +263 -48
- data/Rakefile +71 -1
- data/docs/api/core/index.md +53 -46
- data/docs/api/core/memory.md +200 -154
- data/docs/api/core/network.md +13 -3
- data/docs/api/core/robot.md +490 -130
- data/docs/api/core/state.md +55 -73
- data/docs/api/core/tool.md +205 -209
- data/docs/api/index.md +7 -28
- 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/messages/index.md +35 -20
- data/docs/api/messages/text-message.md +67 -21
- data/docs/api/messages/tool-call-message.md +80 -41
- data/docs/api/messages/tool-result-message.md +119 -50
- data/docs/api/messages/user-message.md +48 -24
- 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 +360 -116
- 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 +311 -49
- 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 +347 -154
- 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 +418 -212
- data/docs/guides/creating-networks.md +143 -24
- data/docs/guides/index.md +0 -5
- data/docs/guides/mcp-integration.md +152 -113
- data/docs/guides/memory.md +220 -164
- data/docs/guides/rails-integration.md +244 -162
- data/docs/guides/streaming.md +137 -187
- data/docs/guides/using-tools.md +259 -212
- data/docs/index.md +46 -41
- data/examples/01_simple_robot.rb +6 -9
- data/examples/02_tools.rb +6 -9
- data/examples/03_network.rb +19 -17
- 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 +169 -0
- data/examples/09_chaining.rb +262 -0
- data/examples/10_memory.rb +331 -0
- data/examples/11_network_introspection.rb +253 -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 +63 -0
- data/examples/14_rusty_circuit/open_mic.rb +123 -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 +156 -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/16_writers_room/display.rb +158 -0
- data/examples/16_writers_room/output/.gitignore +2 -0
- data/examples/16_writers_room/output/opus_001.md +263 -0
- data/examples/16_writers_room/output/opus_001_notes.log +470 -0
- data/examples/16_writers_room/prompts/writer.md +37 -0
- data/examples/16_writers_room/room.rb +150 -0
- data/examples/16_writers_room/tools.rb +162 -0
- data/examples/16_writers_room/writer.rb +121 -0
- data/examples/16_writers_room/writers_room.rb +162 -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 +0 -13
- 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 +10 -34
- data/lib/robot_lab/network.rb +13 -20
- data/lib/robot_lab/robot/bus_messaging.rb +239 -0
- data/lib/robot_lab/robot/mcp_management.rb +88 -0
- data/lib/robot_lab/robot/template_rendering.rb +130 -0
- data/lib/robot_lab/robot.rb +240 -330
- data/lib/robot_lab/robot_message.rb +44 -0
- data/lib/robot_lab/robot_result.rb +1 -0
- data/lib/robot_lab/run_config.rb +184 -0
- data/lib/robot_lab/state_proxy.rb +2 -12
- data/lib/robot_lab/streaming/context.rb +1 -1
- data/lib/robot_lab/task.rb +8 -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/utils.rb +39 -0
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +89 -57
- data/mkdocs.yml +0 -11
- metadata +121 -135
- data/docs/api/adapters/anthropic.md +0 -121
- data/docs/api/adapters/gemini.md +0 -133
- data/docs/api/adapters/index.md +0 -104
- data/docs/api/adapters/openai.md +0 -134
- data/docs/api/history/active-record-adapter.md +0 -195
- data/docs/api/history/config.md +0 -191
- data/docs/api/history/index.md +0 -132
- data/docs/api/history/thread-manager.md +0 -144
- data/docs/guides/history.md +0 -359
- 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/adapters/anthropic.rb +0 -163
- data/lib/robot_lab/adapters/base.rb +0 -85
- data/lib/robot_lab/adapters/gemini.rb +0 -193
- data/lib/robot_lab/adapters/openai.rb +0 -159
- data/lib/robot_lab/adapters/registry.rb +0 -81
- data/lib/robot_lab/configuration.rb +0 -143
- data/lib/robot_lab/errors.rb +0 -70
- data/lib/robot_lab/history/active_record_adapter.rb +0 -146
- data/lib/robot_lab/history/config.rb +0 -115
- data/lib/robot_lab/history/thread_manager.rb +0 -93
- data/lib/robot_lab/robotic_model.rb +0 -324
|
@@ -4,129 +4,235 @@ 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)
|
|
69
109
|
|
|
70
110
|
```ruby
|
|
71
|
-
|
|
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.create Factory
|
|
134
|
+
|
|
135
|
+
For simpler tools that do not need a class:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
tool = RobotLab::Tool.create(
|
|
139
|
+
name: "get_time",
|
|
140
|
+
description: "Get the current time"
|
|
141
|
+
) { |_args| Time.now.to_s }
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
With parameter schema:
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
tool = RobotLab::Tool.create(
|
|
72
148
|
name: "get_weather",
|
|
73
|
-
description: "Get
|
|
149
|
+
description: "Get weather for a location",
|
|
74
150
|
parameters: {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
description: "City name"
|
|
78
|
-
required: true
|
|
151
|
+
type: "object",
|
|
152
|
+
properties: {
|
|
153
|
+
location: { type: "string", description: "City name" }
|
|
79
154
|
},
|
|
80
|
-
|
|
81
|
-
type: "string",
|
|
82
|
-
enum: ["celsius", "fahrenheit"],
|
|
83
|
-
default: "celsius"
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
handler: ->(location:, unit: "celsius", **_context) {
|
|
87
|
-
WeatherAPI.current(location, unit: unit)
|
|
155
|
+
required: ["location"]
|
|
88
156
|
}
|
|
89
|
-
)
|
|
157
|
+
) { |args| WeatherAPI.current(args[:location]) }
|
|
90
158
|
```
|
|
91
159
|
|
|
92
160
|
### Tool Execution
|
|
93
161
|
|
|
94
162
|
When an LLM decides to use a tool:
|
|
95
163
|
|
|
96
|
-
1. LLM generates a
|
|
97
|
-
2.
|
|
98
|
-
3.
|
|
99
|
-
4. Result is
|
|
100
|
-
5.
|
|
164
|
+
1. LLM generates a tool call with tool name and arguments
|
|
165
|
+
2. `@chat` (RubyLLM) identifies the tool from its registered tools
|
|
166
|
+
3. Calls the `execute` method with keyword arguments
|
|
167
|
+
4. Result is sent back to the LLM for continued processing
|
|
168
|
+
5. Loop repeats until the LLM produces a final text response
|
|
169
|
+
|
|
170
|
+
### Error Handling
|
|
171
|
+
|
|
172
|
+
Tool errors are captured and returned to the LLM:
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
def execute(order_id:)
|
|
176
|
+
order = ORDERS[order_id]
|
|
177
|
+
if order
|
|
178
|
+
order
|
|
179
|
+
else
|
|
180
|
+
{ error: "Order not found" }
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
```
|
|
101
184
|
|
|
102
|
-
|
|
185
|
+
## Memory
|
|
103
186
|
|
|
104
|
-
|
|
187
|
+
Memory is a reactive key-value store used by robots and networks.
|
|
188
|
+
|
|
189
|
+
### Standalone vs Network Memory
|
|
190
|
+
|
|
191
|
+
- **Standalone**: Each robot has its own inherent `Memory` instance (`robot.memory`)
|
|
192
|
+
- **In a Network**: All robots share the network's `Memory` instance
|
|
105
193
|
|
|
106
194
|
```ruby
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
#
|
|
110
|
-
|
|
111
|
-
|
|
195
|
+
# Standalone memory
|
|
196
|
+
robot.memory[:user_id] = 123
|
|
197
|
+
robot.memory[:user_id] # => 123
|
|
198
|
+
|
|
199
|
+
# Network memory is passed automatically
|
|
200
|
+
network = RobotLab.create_network(name: "pipeline") do
|
|
201
|
+
task :robot_a, robot_a, depends_on: :none
|
|
202
|
+
task :robot_b, robot_b, depends_on: [:robot_a]
|
|
203
|
+
end
|
|
204
|
+
# Both robot_a and robot_b share network.memory during execution
|
|
112
205
|
```
|
|
113
206
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
207
|
+
### Reserved Keys
|
|
208
|
+
|
|
209
|
+
| Key | Type | Description |
|
|
210
|
+
|-----|------|-------------|
|
|
211
|
+
| `:data` | `Hash` | Runtime data (accessible via `memory.data.key_name`) |
|
|
212
|
+
| `:results` | `Array` | Accumulated robot results |
|
|
213
|
+
| `:messages` | `Array` | Conversation history |
|
|
214
|
+
| `:session_id` | `String` | Session identifier |
|
|
215
|
+
| `:cache` | `Module` | Semantic cache (RubyLLM::SemanticCache) |
|
|
119
216
|
|
|
120
|
-
|
|
217
|
+
### Reactive Features
|
|
121
218
|
|
|
122
|
-
|
|
219
|
+
Memory supports pub/sub semantics for inter-robot communication:
|
|
123
220
|
|
|
124
221
|
```ruby
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
)
|
|
222
|
+
# Write a value (notifies subscribers, wakes waiters)
|
|
223
|
+
memory.set(:sentiment, { score: 0.8 })
|
|
224
|
+
|
|
225
|
+
# Read a value (non-blocking)
|
|
226
|
+
memory.get(:sentiment) # => { score: 0.8 } or nil
|
|
227
|
+
|
|
228
|
+
# Blocking read (waits until value exists)
|
|
229
|
+
memory.get(:sentiment, wait: true) # Blocks indefinitely
|
|
230
|
+
memory.get(:sentiment, wait: 30) # Blocks up to 30 seconds
|
|
231
|
+
|
|
232
|
+
# Subscribe to changes
|
|
233
|
+
memory.subscribe(:sentiment) do |change|
|
|
234
|
+
puts "#{change.key} = #{change.value} (written by #{change.writer})"
|
|
235
|
+
end
|
|
130
236
|
```
|
|
131
237
|
|
|
132
238
|
## Message Types
|
|
@@ -136,27 +242,22 @@ RobotLab uses a type hierarchy for messages:
|
|
|
136
242
|
```mermaid
|
|
137
243
|
classDiagram
|
|
138
244
|
Message <|-- TextMessage
|
|
139
|
-
Message <|--
|
|
140
|
-
|
|
141
|
-
ToolMessage <|-- ToolResultMessage
|
|
245
|
+
Message <|-- ToolCallMessage
|
|
246
|
+
Message <|-- ToolResultMessage
|
|
142
247
|
|
|
143
248
|
class Message {
|
|
144
249
|
+String type
|
|
145
250
|
+String role
|
|
146
|
-
+
|
|
251
|
+
+content
|
|
147
252
|
+String stop_reason
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
class TextMessage {
|
|
151
253
|
+text?()
|
|
152
|
-
+
|
|
153
|
-
+
|
|
254
|
+
+tool_call?()
|
|
255
|
+
+tool_result?()
|
|
256
|
+
+stopped?()
|
|
154
257
|
}
|
|
155
258
|
|
|
156
|
-
class
|
|
157
|
-
+String
|
|
158
|
-
+String name
|
|
159
|
-
+Hash input
|
|
259
|
+
class TextMessage {
|
|
260
|
+
+String content
|
|
160
261
|
}
|
|
161
262
|
|
|
162
263
|
class ToolCallMessage {
|
|
@@ -166,6 +267,8 @@ classDiagram
|
|
|
166
267
|
class ToolResultMessage {
|
|
167
268
|
+ToolMessage tool
|
|
168
269
|
+Hash content
|
|
270
|
+
+success?()
|
|
271
|
+
+error?()
|
|
169
272
|
}
|
|
170
273
|
```
|
|
171
274
|
|
|
@@ -176,7 +279,7 @@ classDiagram
|
|
|
176
279
|
| `user` | Input from the user |
|
|
177
280
|
| `assistant` | Response from the LLM |
|
|
178
281
|
| `system` | System instructions |
|
|
179
|
-
| `
|
|
282
|
+
| `tool_result` | Tool execution result |
|
|
180
283
|
|
|
181
284
|
### Stop Reasons
|
|
182
285
|
|
|
@@ -184,60 +287,201 @@ classDiagram
|
|
|
184
287
|
|--------|-------------|
|
|
185
288
|
| `stop` | Natural completion |
|
|
186
289
|
| `tool` | Tool call requested |
|
|
187
|
-
| `max_tokens` | Token limit reached |
|
|
188
290
|
|
|
189
291
|
## RobotResult
|
|
190
292
|
|
|
191
293
|
The output from a robot execution:
|
|
192
294
|
|
|
193
295
|
```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
|
|
296
|
+
result = robot.run("Hello!")
|
|
297
|
+
|
|
298
|
+
result.robot_name # => "support_agent"
|
|
299
|
+
result.output # => [TextMessage, ...]
|
|
300
|
+
result.tool_calls # => [ToolResultMessage, ...]
|
|
301
|
+
result.stop_reason # => "stop"
|
|
302
|
+
result.created_at # => Time
|
|
303
|
+
result.id # => UUID string
|
|
201
304
|
```
|
|
202
305
|
|
|
203
306
|
### Accessing Response Content
|
|
204
307
|
|
|
205
308
|
```ruby
|
|
206
|
-
# Get text response
|
|
207
|
-
text = result.
|
|
309
|
+
# Get last text response (most common)
|
|
310
|
+
text = result.last_text_content
|
|
208
311
|
|
|
209
312
|
# Check if tools were called
|
|
210
|
-
has_tools = result.
|
|
313
|
+
has_tools = result.has_tool_calls?
|
|
314
|
+
|
|
315
|
+
# Check if execution completed naturally
|
|
316
|
+
result.stopped?
|
|
211
317
|
|
|
212
|
-
#
|
|
213
|
-
|
|
318
|
+
# Serialization
|
|
319
|
+
result.export # => Hash (excludes debug fields)
|
|
320
|
+
result.to_h # => Hash (includes debug fields)
|
|
321
|
+
result.to_json # => JSON string
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Configuration
|
|
325
|
+
|
|
326
|
+
RobotLab uses `MywayConfig` for configuration. There is no `RobotLab.configure` block. Configuration is loaded from:
|
|
327
|
+
|
|
328
|
+
1. Bundled defaults (`lib/robot_lab/config/defaults.yml`)
|
|
329
|
+
2. Environment-specific overrides
|
|
330
|
+
3. XDG config files (`~/.config/robot_lab/config.yml`)
|
|
331
|
+
4. Project config (`./config/robot_lab.yml`)
|
|
332
|
+
5. Environment variables (`ROBOT_LAB_*` prefix)
|
|
333
|
+
|
|
334
|
+
Access via `RobotLab.config`:
|
|
335
|
+
|
|
336
|
+
```ruby
|
|
337
|
+
RobotLab.config.ruby_llm.model # => "claude-sonnet-4"
|
|
338
|
+
RobotLab.config.ruby_llm.request_timeout # => 120
|
|
214
339
|
```
|
|
215
340
|
|
|
216
341
|
## Configuration Hierarchy
|
|
217
342
|
|
|
218
|
-
|
|
343
|
+
Tools and MCP servers use a cascading configuration system:
|
|
344
|
+
|
|
345
|
+
```
|
|
346
|
+
RobotLab.config (global)
|
|
347
|
+
|
|
|
348
|
+
+-- mcp: [server1, server2]
|
|
349
|
+
+-- tools: [tool1, tool2]
|
|
350
|
+
|
|
|
351
|
+
+-- Network
|
|
352
|
+
| |
|
|
353
|
+
| +-- mcp: :inherit | :none | [servers]
|
|
354
|
+
| +-- tools: :inherit | :none | [tools]
|
|
355
|
+
| |
|
|
356
|
+
| +-- Task (per-step config)
|
|
357
|
+
| | +-- context: { department: "billing" }
|
|
358
|
+
| | +-- mcp: :none | :inherit | [servers]
|
|
359
|
+
| | +-- tools: :none | :inherit | [tools]
|
|
360
|
+
| |
|
|
361
|
+
| +-- Robot (build-time config)
|
|
362
|
+
| |
|
|
363
|
+
| +-- mcp: :inherit | :none | [servers]
|
|
364
|
+
| +-- tools: :inherit | :none | [tools]
|
|
365
|
+
| |
|
|
366
|
+
| +-- run() call (runtime config)
|
|
367
|
+
| +-- mcp: :none | [servers]
|
|
368
|
+
| +-- tools: :none | [tools]
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Resolution order: **runtime > robot build-time > task > network > global config**.
|
|
372
|
+
|
|
373
|
+
The `:inherit` value pulls from the parent level. `:none` explicitly disables.
|
|
374
|
+
|
|
375
|
+
## Message Bus
|
|
376
|
+
|
|
377
|
+
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.
|
|
378
|
+
|
|
379
|
+
### How It Works
|
|
380
|
+
|
|
381
|
+
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.
|
|
382
|
+
|
|
383
|
+
```ruby
|
|
384
|
+
bus = TypedBus::MessageBus.new
|
|
385
|
+
|
|
386
|
+
bob = RobotLab.build(name: "bob", system_prompt: "You tell jokes.", bus: bus)
|
|
387
|
+
alice = RobotLab.build(name: "alice", system_prompt: "You evaluate jokes.", bus: bus)
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### RobotMessage
|
|
391
|
+
|
|
392
|
+
`RobotMessage` is an immutable `Data.define` value object used as the typed envelope:
|
|
393
|
+
|
|
394
|
+
| Field | Type | Description |
|
|
395
|
+
|-------|------|-------------|
|
|
396
|
+
| `id` | `Integer` | Per-robot sequential counter |
|
|
397
|
+
| `from` | `String` | Sender's robot name (= channel name) |
|
|
398
|
+
| `content` | `String`, `Hash` | Message payload |
|
|
399
|
+
| `in_reply_to` | `String`, `nil` | Composite key of the original message (e.g., `"alice:1"`) |
|
|
400
|
+
|
|
401
|
+
Methods: `key` returns `"from:id"` composite identity; `reply?` returns true when `in_reply_to` is set.
|
|
219
402
|
|
|
403
|
+
### Sending and Receiving
|
|
404
|
+
|
|
405
|
+
```ruby
|
|
406
|
+
# Send a message to another robot
|
|
407
|
+
alice.send_message(to: :bob, content: "Tell me a joke.")
|
|
408
|
+
|
|
409
|
+
# Handle incoming messages with auto-ack (1 arg)
|
|
410
|
+
bob.on_message do |message|
|
|
411
|
+
joke = bob.run(message.content.to_s).last_text_content
|
|
412
|
+
bob.send_reply(to: message.from.to_sym, content: joke, in_reply_to: message.key)
|
|
413
|
+
end
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Block arity controls delivery handling: 1 argument auto-acks; 2 arguments give manual control over `delivery.ack!`/`delivery.nack!`.
|
|
417
|
+
|
|
418
|
+
### Dynamic Spawning
|
|
419
|
+
|
|
420
|
+
Robots can create new robots at runtime using `spawn`. The bus is created lazily — no upfront wiring required:
|
|
421
|
+
|
|
422
|
+
```ruby
|
|
423
|
+
dispatcher = RobotLab.build(name: "dispatcher", system_prompt: "You delegate work.")
|
|
424
|
+
|
|
425
|
+
# spawn creates a child on the same bus (bus created automatically)
|
|
426
|
+
helper = dispatcher.spawn(name: "helper", system_prompt: "You answer questions.")
|
|
427
|
+
|
|
428
|
+
# The child can immediately communicate with the parent
|
|
429
|
+
answer = helper.run("What is 2+2?").last_text_content
|
|
430
|
+
helper.send_message(to: :dispatcher, content: answer)
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Robots can also join a bus after creation using `with_bus`:
|
|
434
|
+
|
|
435
|
+
```ruby
|
|
436
|
+
bot = RobotLab.build(name: "late_joiner", system_prompt: "Hello.")
|
|
437
|
+
bot.with_bus(existing_bus) # now connected to the bus
|
|
220
438
|
```
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
├── tools: :inherit | :none | [tools]
|
|
230
|
-
│
|
|
231
|
-
└── Robot
|
|
232
|
-
│
|
|
233
|
-
├── mcp: :inherit | :none | [servers]
|
|
234
|
-
└── tools: :inherit | :none | [tools]
|
|
439
|
+
|
|
440
|
+
**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:
|
|
441
|
+
|
|
442
|
+
```ruby
|
|
443
|
+
worker1 = dispatcher.spawn(name: "worker", system_prompt: "Worker 1")
|
|
444
|
+
worker2 = dispatcher.spawn(name: "worker", system_prompt: "Worker 2")
|
|
445
|
+
dispatcher.send_message(to: :worker, content: "Do this task")
|
|
446
|
+
# Both worker1 and worker2 receive the message
|
|
235
447
|
```
|
|
236
448
|
|
|
237
|
-
|
|
449
|
+
### Bus vs Network
|
|
450
|
+
|
|
451
|
+
| Feature | Network | Message Bus |
|
|
452
|
+
|---------|---------|-------------|
|
|
453
|
+
| Execution model | DAG (acyclic) | Cyclic, bidirectional |
|
|
454
|
+
| Communication | Sequential pipeline | Pub/sub channels |
|
|
455
|
+
| Memory | Shared network memory | Independent per-robot |
|
|
456
|
+
| Use case | Linear workflows | Negotiation, convergence |
|
|
457
|
+
|
|
458
|
+
The bus is purely additive — robots without `bus:` work exactly as before.
|
|
459
|
+
|
|
460
|
+
## Network
|
|
461
|
+
|
|
462
|
+
A Network orchestrates multiple robots in a pipeline workflow using SimpleFlow:
|
|
463
|
+
|
|
464
|
+
```ruby
|
|
465
|
+
network = RobotLab.create_network(name: "support") do
|
|
466
|
+
task :classifier, classifier_robot, depends_on: :none
|
|
467
|
+
task :billing, billing_robot, depends_on: :optional
|
|
468
|
+
task :technical, technical_robot, depends_on: :optional
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
result = network.run(message: "I need help with billing")
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Networks provide:
|
|
475
|
+
|
|
476
|
+
- **DAG-based execution** via SimpleFlow with `depends_on:` for sequencing
|
|
477
|
+
- **Parallel execution** for tasks with the same dependencies
|
|
478
|
+
- **Optional tasks** activated dynamically by classifier robots
|
|
479
|
+
- **Shared memory** for inter-robot communication
|
|
480
|
+
- **Per-task configuration** via the `Task` wrapper
|
|
481
|
+
- **Broadcast messaging** for network-wide announcements
|
|
238
482
|
|
|
239
483
|
## Next Steps
|
|
240
484
|
|
|
241
485
|
- [Robot Execution](robot-execution.md) - Detailed execution flow
|
|
242
486
|
- [Network Orchestration](network-orchestration.md) - Multi-robot coordination
|
|
243
|
-
- [
|
|
487
|
+
- [Using Tools](../guides/using-tools.md) - Creating and using tools
|