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.
Files changed (145) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/deploy-github-pages.yml +9 -9
  3. data/.irbrc +6 -0
  4. data/CHANGELOG.md +90 -0
  5. data/README.md +203 -46
  6. data/Rakefile +70 -1
  7. data/docs/api/core/index.md +12 -0
  8. data/docs/api/core/robot.md +478 -130
  9. data/docs/api/core/tool.md +205 -209
  10. data/docs/api/history/active-record-adapter.md +174 -94
  11. data/docs/api/history/config.md +186 -93
  12. data/docs/api/history/index.md +57 -61
  13. data/docs/api/history/thread-manager.md +123 -73
  14. data/docs/api/mcp/client.md +119 -48
  15. data/docs/api/mcp/index.md +75 -60
  16. data/docs/api/mcp/server.md +120 -136
  17. data/docs/api/mcp/transports.md +172 -184
  18. data/docs/api/streaming/context.md +157 -74
  19. data/docs/api/streaming/events.md +114 -166
  20. data/docs/api/streaming/index.md +74 -72
  21. data/docs/architecture/core-concepts.md +361 -112
  22. data/docs/architecture/index.md +97 -59
  23. data/docs/architecture/message-flow.md +138 -129
  24. data/docs/architecture/network-orchestration.md +197 -50
  25. data/docs/architecture/robot-execution.md +199 -146
  26. data/docs/architecture/state-management.md +255 -187
  27. data/docs/concepts.md +312 -48
  28. data/docs/examples/basic-chat.md +89 -77
  29. data/docs/examples/index.md +222 -47
  30. data/docs/examples/mcp-server.md +207 -203
  31. data/docs/examples/multi-robot-network.md +129 -35
  32. data/docs/examples/rails-application.md +159 -160
  33. data/docs/examples/tool-usage.md +295 -204
  34. data/docs/getting-started/configuration.md +275 -162
  35. data/docs/getting-started/index.md +1 -1
  36. data/docs/getting-started/installation.md +22 -13
  37. data/docs/getting-started/quick-start.md +166 -121
  38. data/docs/guides/building-robots.md +417 -212
  39. data/docs/guides/creating-networks.md +94 -24
  40. data/docs/guides/mcp-integration.md +152 -113
  41. data/docs/guides/memory.md +220 -164
  42. data/docs/guides/streaming.md +80 -110
  43. data/docs/guides/using-tools.md +259 -212
  44. data/docs/index.md +50 -37
  45. data/examples/01_simple_robot.rb +6 -9
  46. data/examples/02_tools.rb +6 -9
  47. data/examples/03_network.rb +13 -14
  48. data/examples/04_mcp.rb +5 -8
  49. data/examples/05_streaming.rb +5 -8
  50. data/examples/06_prompt_templates.rb +42 -37
  51. data/examples/07_network_memory.rb +13 -14
  52. data/examples/08_llm_config.rb +140 -0
  53. data/examples/09_chaining.rb +223 -0
  54. data/examples/10_memory.rb +331 -0
  55. data/examples/11_network_introspection.rb +230 -0
  56. data/examples/12_message_bus.rb +74 -0
  57. data/examples/13_spawn.rb +90 -0
  58. data/examples/14_rusty_circuit/comic.rb +143 -0
  59. data/examples/14_rusty_circuit/display.rb +203 -0
  60. data/examples/14_rusty_circuit/heckler.rb +57 -0
  61. data/examples/14_rusty_circuit/open_mic.rb +121 -0
  62. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +20 -0
  63. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +23 -0
  64. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +20 -0
  65. data/examples/14_rusty_circuit/scout.rb +173 -0
  66. data/examples/14_rusty_circuit/scout_notes.md +89 -0
  67. data/examples/14_rusty_circuit/show.log +234 -0
  68. data/examples/15_memory_network_and_bus/editor_in_chief.rb +24 -0
  69. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +206 -0
  70. data/examples/15_memory_network_and_bus/linux_writer.rb +80 -0
  71. data/examples/15_memory_network_and_bus/os_editor.rb +46 -0
  72. data/examples/15_memory_network_and_bus/os_writer.rb +46 -0
  73. data/examples/15_memory_network_and_bus/output/combined_article.md +13 -0
  74. data/examples/15_memory_network_and_bus/output/final_article.md +15 -0
  75. data/examples/15_memory_network_and_bus/output/linux_draft.md +5 -0
  76. data/examples/15_memory_network_and_bus/output/mac_draft.md +7 -0
  77. data/examples/15_memory_network_and_bus/output/memory.json +13 -0
  78. data/examples/15_memory_network_and_bus/output/revision_1.md +19 -0
  79. data/examples/15_memory_network_and_bus/output/revision_2.md +15 -0
  80. data/examples/15_memory_network_and_bus/output/windows_draft.md +7 -0
  81. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +13 -0
  82. data/examples/15_memory_network_and_bus/prompts/os_chief.md +13 -0
  83. data/examples/15_memory_network_and_bus/prompts/os_editor.md +13 -0
  84. data/examples/README.md +197 -0
  85. data/examples/prompts/{assistant/system.txt.erb → assistant.md} +3 -0
  86. data/examples/prompts/{billing/system.txt.erb → billing.md} +3 -0
  87. data/examples/prompts/{classifier/system.txt.erb → classifier.md} +3 -0
  88. data/examples/prompts/comedian.md +6 -0
  89. data/examples/prompts/comedy_critic.md +10 -0
  90. data/examples/prompts/configurable.md +9 -0
  91. data/examples/prompts/dispatcher.md +12 -0
  92. data/examples/prompts/{entity_extractor/system.txt.erb → entity_extractor.md} +3 -0
  93. data/examples/prompts/{escalation/system.txt.erb → escalation.md} +7 -0
  94. data/examples/prompts/frontmatter_mcp_test.md +9 -0
  95. data/examples/prompts/frontmatter_named_test.md +5 -0
  96. data/examples/prompts/frontmatter_tools_test.md +6 -0
  97. data/examples/prompts/{general/system.txt.erb → general.md} +3 -0
  98. data/examples/prompts/{github_assistant/system.txt.erb → github_assistant.md} +8 -0
  99. data/examples/prompts/{helper/system.txt.erb → helper.md} +3 -0
  100. data/examples/prompts/{keyword_extractor/system.txt.erb → keyword_extractor.md} +3 -0
  101. data/examples/prompts/llm_config_demo.md +20 -0
  102. data/examples/prompts/{order_support/system.txt.erb → order_support.md} +8 -0
  103. data/examples/prompts/os_advocate.md +13 -0
  104. data/examples/prompts/os_chief.md +13 -0
  105. data/examples/prompts/os_editor.md +13 -0
  106. data/examples/prompts/{product_support/system.txt.erb → product_support.md} +7 -0
  107. data/examples/prompts/{sentiment_analyzer/system.txt.erb → sentiment_analyzer.md} +3 -0
  108. data/examples/prompts/{synthesizer/system.txt.erb → synthesizer.md} +3 -0
  109. data/examples/prompts/{technical/system.txt.erb → technical.md} +3 -0
  110. data/examples/prompts/{triage/system.txt.erb → triage.md} +6 -0
  111. data/lib/generators/robot_lab/templates/initializer.rb.tt +1 -1
  112. data/lib/robot_lab/adapters/openai.rb +2 -1
  113. data/lib/robot_lab/ask_user.rb +75 -0
  114. data/lib/robot_lab/config/defaults.yml +121 -0
  115. data/lib/robot_lab/config.rb +183 -0
  116. data/lib/robot_lab/error.rb +6 -0
  117. data/lib/robot_lab/mcp/client.rb +1 -1
  118. data/lib/robot_lab/memory.rb +2 -2
  119. data/lib/robot_lab/robot.rb +523 -249
  120. data/lib/robot_lab/robot_message.rb +44 -0
  121. data/lib/robot_lab/robot_result.rb +1 -0
  122. data/lib/robot_lab/robotic_model.rb +1 -1
  123. data/lib/robot_lab/streaming/context.rb +1 -1
  124. data/lib/robot_lab/tool.rb +108 -172
  125. data/lib/robot_lab/tool_config.rb +1 -1
  126. data/lib/robot_lab/tool_manifest.rb +2 -18
  127. data/lib/robot_lab/version.rb +1 -1
  128. data/lib/robot_lab.rb +66 -55
  129. metadata +107 -116
  130. data/examples/prompts/assistant/user.txt.erb +0 -1
  131. data/examples/prompts/billing/user.txt.erb +0 -1
  132. data/examples/prompts/classifier/user.txt.erb +0 -1
  133. data/examples/prompts/entity_extractor/user.txt.erb +0 -3
  134. data/examples/prompts/escalation/user.txt.erb +0 -34
  135. data/examples/prompts/general/user.txt.erb +0 -1
  136. data/examples/prompts/github_assistant/user.txt.erb +0 -1
  137. data/examples/prompts/helper/user.txt.erb +0 -1
  138. data/examples/prompts/keyword_extractor/user.txt.erb +0 -3
  139. data/examples/prompts/order_support/user.txt.erb +0 -22
  140. data/examples/prompts/product_support/user.txt.erb +0 -32
  141. data/examples/prompts/sentiment_analyzer/user.txt.erb +0 -3
  142. data/examples/prompts/synthesizer/user.txt.erb +0 -15
  143. data/examples/prompts/technical/user.txt.erb +0 -1
  144. data/examples/prompts/triage/user.txt.erb +0 -17
  145. data/lib/robot_lab/configuration.rb +0 -143
@@ -1,269 +1,617 @@
1
1
  # Robot
2
2
 
3
- LLM-powered agent with personality, tools, and model configuration.
3
+ LLM-powered agent with template-based prompts, tools, memory, and MCP integration.
4
4
 
5
- ## Class: `RobotLab::Robot`
5
+ ## Class Hierarchy
6
6
 
7
- ```ruby
8
- robot = RobotLab.build do
9
- name "assistant"
10
- description "A helpful assistant"
11
- model "claude-sonnet-4"
12
- template "You are helpful."
13
- end
7
+ ```
8
+ RubyLLM::Agent
9
+ └── RobotLab::Robot
10
+ └── Your custom subclasses (e.g., ClassifierRobot)
14
11
  ```
15
12
 
16
- ## Attributes
13
+ `Robot` inherits from `RubyLLM::Agent`, which creates a persistent `@chat` on initialization. The robot adds template-based prompts, shared memory, hierarchical MCP configuration, and SimpleFlow pipeline integration on top of the base agent.
17
14
 
18
- ### name
15
+ ## Constructor
19
16
 
20
17
  ```ruby
21
- robot.name # => String
18
+ Robot.new(
19
+ name:,
20
+ template: nil,
21
+ system_prompt: nil,
22
+ context: {},
23
+ description: nil,
24
+ local_tools: [],
25
+ model: nil,
26
+ mcp_servers: [],
27
+ mcp: :none,
28
+ tools: :none,
29
+ on_tool_call: nil,
30
+ on_tool_result: nil,
31
+ enable_cache: true,
32
+ bus: nil,
33
+ temperature: nil,
34
+ top_p: nil,
35
+ top_k: nil,
36
+ max_tokens: nil,
37
+ presence_penalty: nil,
38
+ frequency_penalty: nil,
39
+ stop: nil
40
+ )
22
41
  ```
23
42
 
24
- Unique identifier for the robot within a network.
25
-
26
- ### description
43
+ ### Parameters
44
+
45
+ | Name | Type | Default | Description |
46
+ |------|------|---------|-------------|
47
+ | `name` | `String` | **required** | Unique identifier for the robot |
48
+ | `template` | `Symbol`, `nil` | `nil` | Prompt template (e.g., `:assistant` loads `prompts/assistant.md`) |
49
+ | `system_prompt` | `String`, `nil` | `nil` | Inline system prompt (appended after template if both given) |
50
+ | `context` | `Hash`, `Proc` | `{}` | Variables passed to the template |
51
+ | `description` | `String`, `nil` | `nil` | Human-readable description of what the robot does |
52
+ | `local_tools` | `Array` | `[]` | Tools defined locally (`RubyLLM::Tool` subclasses or `RobotLab::Tool` instances) |
53
+ | `model` | `String`, `nil` | `nil` | LLM model ID (falls back to `RobotLab.config.ruby_llm.model`) |
54
+ | `mcp_servers` | `Array` | `[]` | Legacy MCP server configurations |
55
+ | `mcp` | `Symbol`, `Array` | `:none` | Hierarchical MCP config (`:none`, `:inherit`, or server array) |
56
+ | `tools` | `Symbol`, `Array` | `:none` | Hierarchical tools config (`:none`, `:inherit`, or tool name array) |
57
+ | `on_tool_call` | `Proc`, `nil` | `nil` | Callback invoked when a tool is called |
58
+ | `on_tool_result` | `Proc`, `nil` | `nil` | Callback invoked when a tool returns a result |
59
+ | `enable_cache` | `Boolean` | `true` | Whether to enable semantic caching |
60
+ | `bus` | `TypedBus::MessageBus`, `nil` | `nil` | Optional message bus for inter-robot communication |
61
+ | `temperature` | `Float`, `nil` | `nil` | Controls randomness (0.0-1.0) |
62
+ | `top_p` | `Float`, `nil` | `nil` | Nucleus sampling threshold |
63
+ | `top_k` | `Integer`, `nil` | `nil` | Top-k sampling |
64
+ | `max_tokens` | `Integer`, `nil` | `nil` | Maximum tokens in response |
65
+ | `presence_penalty` | `Float`, `nil` | `nil` | Penalize based on presence |
66
+ | `frequency_penalty` | `Float`, `nil` | `nil` | Penalize based on frequency |
67
+ | `stop` | `String`, `Array`, `nil` | `nil` | Stop sequences |
68
+
69
+ ## Factory Method
27
70
 
28
71
  ```ruby
29
- robot.description # => String
72
+ robot = RobotLab.build(
73
+ name: "robot", # Defaults to "robot"
74
+ template: nil,
75
+ system_prompt: nil,
76
+ context: {},
77
+ enable_cache: true,
78
+ bus: nil, # Optional TypedBus::MessageBus
79
+ **options # All other Robot.new parameters
80
+ )
81
+ # => RobotLab::Robot
30
82
  ```
31
83
 
32
- Human-readable description of what the robot does.
84
+ If `name` is omitted, it defaults to `"robot"`.
33
85
 
34
- ### model
86
+ ## Attributes (Read-Only)
35
87
 
36
- ```ruby
37
- robot.model # => String
38
- ```
88
+ | Attribute | Type | Description |
89
+ |-----------|------|-------------|
90
+ | `name` | `String` | Unique identifier |
91
+ | `description` | `String`, `nil` | Human-readable description |
92
+ | `template` | `Symbol`, `nil` | Prompt template identifier |
93
+ | `system_prompt` | `String`, `nil` | Inline system prompt |
94
+ | `local_tools` | `Array` | Locally defined tools |
95
+ | `mcp_clients` | `Hash<String, MCP::Client>` | Connected MCP clients, keyed by server name |
96
+ | `mcp_tools` | `Array<Tool>` | Tools discovered from MCP servers |
97
+ | `memory` | `Memory` | Inherent memory (used when standalone, not in network) |
98
+ | `bus` | `TypedBus::MessageBus`, `nil` | Message bus instance (nil if not configured) |
99
+ | `outbox` | `Hash` | Sent messages tracked by composite key with status and replies |
100
+ | `mcp_config` | `Symbol`, `Array` | Build-time MCP configuration (raw, unresolved) |
101
+ | `tools_config` | `Symbol`, `Array` | Build-time tools configuration (raw, unresolved) |
39
102
 
40
- LLM model identifier (e.g., "claude-sonnet-4", "gpt-4o").
103
+ ## Attributes (Read-Write)
41
104
 
42
- ### template
105
+ | Attribute | Type | Default | Description |
106
+ |-----------|------|---------|-------------|
107
+ | `input` | `IO`, `nil` | `nil` | Input stream for user interaction (falls back to `$stdin`) |
108
+ | `output` | `IO`, `nil` | `nil` | Output stream for user interaction (falls back to `$stdout`) |
109
+
110
+ Used by tools like [`AskUser`](tool.md#built-in-askuser) that need terminal IO. Set to `StringIO` for testing.
111
+
112
+ ## Methods
113
+
114
+ ### run
43
115
 
44
116
  ```ruby
45
- robot.template # => String
117
+ result = robot.run(message, **kwargs)
118
+ # => RobotResult
46
119
  ```
47
120
 
48
- System prompt that defines the robot's personality.
121
+ Primary execution method. Sends a message to the LLM with memory/MCP/tools resolution and returns a `RobotResult`.
122
+
123
+ **Parameters:**
49
124
 
50
- ### local_tools
125
+ | Name | Type | Default | Description |
126
+ |------|------|---------|-------------|
127
+ | `message` | `String` | **required** | The user message to send |
128
+ | `network` | `NetworkRun`, `nil` | `nil` | Network context (passed internally) |
129
+ | `network_memory` | `Memory`, `nil` | `nil` | Shared network memory |
130
+ | `memory` | `Memory`, `Hash`, `nil` | `nil` | Runtime memory to merge |
131
+ | `mcp` | `Symbol`, `Array` | `:none` | Runtime MCP override |
132
+ | `tools` | `Symbol`, `Array` | `:none` | Runtime tools override |
133
+ | `**kwargs` | `Hash` | `{}` | Additional keyword arguments passed to `Agent#ask` |
134
+
135
+ **Returns:** `RobotResult`
136
+
137
+ **Examples:**
51
138
 
52
139
  ```ruby
53
- robot.local_tools # => Array<Tool>
54
- ```
140
+ # Simple message
141
+ result = robot.run("What is 2+2?")
55
142
 
56
- Tools defined directly on the robot.
143
+ # With runtime memory
144
+ result = robot.run("Summarize the data", memory: { data: report })
57
145
 
58
- ### mcp_clients
146
+ # With streaming block
147
+ result = robot.run("Tell me a story") { |event| print event.text }
148
+
149
+ # With runtime overrides
150
+ result = robot.run("Help me", mcp: :none, tools: :none)
151
+ ```
152
+
153
+ ### model
59
154
 
60
155
  ```ruby
61
- robot.mcp_clients # => Array<MCP::Client>
156
+ robot.model # => "claude-sonnet-4" or nil
62
157
  ```
63
158
 
64
- Connected MCP server clients.
159
+ Returns the model ID string. Resolves through the underlying chat object.
65
160
 
66
- ### mcp_tools
161
+ ### update
67
162
 
68
163
  ```ruby
69
- robot.mcp_tools # => Array<Tool>
164
+ robot.update(
165
+ template: nil,
166
+ context: nil,
167
+ system_prompt: nil,
168
+ model: nil,
169
+ temperature: nil,
170
+ **kwargs
171
+ )
172
+ # => self
70
173
  ```
71
174
 
72
- Tools discovered from MCP servers.
175
+ Reconfigure the robot after construction. Returns `self` for chaining.
176
+
177
+ ### with_* Methods (Chaining)
178
+
179
+ All `with_*` methods delegate to the persistent `@chat` and return `self` for chaining:
180
+
181
+ | Method | Description |
182
+ |--------|-------------|
183
+ | `with_model(model_id)` | Change the LLM model |
184
+ | `with_temperature(temp)` | Set temperature |
185
+ | `with_top_p(value)` | Set nucleus sampling |
186
+ | `with_top_k(value)` | Set top-k sampling |
187
+ | `with_max_tokens(value)` | Set max response tokens |
188
+ | `with_presence_penalty(value)` | Set presence penalty |
189
+ | `with_frequency_penalty(value)` | Set frequency penalty |
190
+ | `with_stop(sequences)` | Set stop sequences |
191
+ | `with_instructions(prompt)` | Set system instructions |
192
+ | `with_tool(tool)` | Add a single tool |
193
+ | `with_tools(*tools)` | Add multiple tools |
194
+ | `with_params(**params)` | Set additional parameters |
195
+ | `with_headers(**headers)` | Set custom headers |
196
+ | `with_schema(schema)` | Set output schema |
197
+ | `with_context(**ctx)` | Set context |
198
+ | `with_thinking(opts)` | Enable extended thinking |
199
+ | `with_bus(bus)` | Connect to a message bus (creates one if nil) |
200
+
201
+ **Example:**
73
202
 
74
- ### mcp_config
203
+ ```ruby
204
+ robot = RobotLab.build(name: "bot")
205
+ robot
206
+ .with_model("claude-sonnet-4")
207
+ .with_temperature(0.7)
208
+ .with_instructions("Be concise.")
209
+ .run("Hello")
210
+ ```
211
+
212
+ ### with_template
75
213
 
76
214
  ```ruby
77
- robot.mcp_config # => Symbol | Array
215
+ robot.with_template(:assistant, tone: "friendly")
216
+ # => self
78
217
  ```
79
218
 
80
- MCP configuration (`:inherit`, `:none`, or server array).
219
+ Apply a prompt_manager template. Separate from the delegated `with_*` methods because it handles template parsing and front matter config.
81
220
 
82
- ### tools_config
221
+ ### call
83
222
 
84
223
  ```ruby
85
- robot.tools_config # => Symbol | Array
224
+ robot.call(result)
225
+ # => SimpleFlow::Result
86
226
  ```
87
227
 
88
- Tools whitelist configuration (`:inherit`, `:none`, or tool names).
228
+ SimpleFlow step interface. Extracts the message from `result.context[:run_params]`, calls `run`, and wraps the output in a continued `SimpleFlow::Result`.
89
229
 
90
- ## Methods
230
+ Override this method in subclasses for custom routing logic (e.g., classifiers).
91
231
 
92
- ### tools
232
+ ### reset_memory
93
233
 
94
234
  ```ruby
95
- robot.tools # => Array<Tool>
235
+ robot.reset_memory
236
+ # => self
96
237
  ```
97
238
 
98
- Returns all available tools (local + MCP, filtered by whitelist).
239
+ Reset the robot's inherent memory to its initial state.
99
240
 
100
- ### run
241
+ ### send_message
101
242
 
102
243
  ```ruby
103
- result = robot.run(
104
- state: state,
105
- network: network,
106
- streaming: nil,
107
- **context
108
- )
109
- # => RobotResult
244
+ message = robot.send_message(to: :bob, content: "Tell me a joke.")
245
+ # => RobotMessage
110
246
  ```
111
247
 
112
- Execute the robot with the given state.
248
+ Publish a message to another robot's bus channel. Increments the internal message counter, creates a `RobotMessage`, tracks it in the outbox, and publishes to the target channel.
113
249
 
114
250
  **Parameters:**
115
251
 
116
252
  | Name | Type | Description |
117
253
  |------|------|-------------|
118
- | `state` | `State` | Conversation state |
119
- | `network` | `Network`, `NetworkRun`, `nil` | Network context |
120
- | `streaming` | `Proc`, `nil` | Streaming callback |
121
- | `**context` | `Hash` | Additional context |
254
+ | `to` | `String`, `Symbol` | Target robot's channel name |
255
+ | `content` | `String`, `Hash` | Message payload |
122
256
 
123
- **Returns:** `RobotResult`
257
+ **Returns:** `RobotMessage`
124
258
 
125
- ### disconnect
259
+ **Raises:** `BusError` if no bus is configured.
260
+
261
+ ### send_reply
126
262
 
127
263
  ```ruby
128
- robot.disconnect
264
+ reply = robot.send_reply(to: :alice, content: "Here's a joke...", in_reply_to: "alice:1")
265
+ # => RobotMessage
129
266
  ```
130
267
 
131
- Disconnect from all MCP servers.
268
+ Publish a correlated reply to a specific message. The `in_reply_to` composite key links this reply to the original message.
132
269
 
133
- ### to_h
270
+ **Parameters:**
271
+
272
+ | Name | Type | Description |
273
+ |------|------|-------------|
274
+ | `to` | `String`, `Symbol` | Target robot's channel name |
275
+ | `content` | `String`, `Hash` | Reply payload |
276
+ | `in_reply_to` | `String` | Composite key of the original message (e.g., `"alice:1"`) |
277
+
278
+ **Returns:** `RobotMessage`
279
+
280
+ **Raises:** `BusError` if no bus is configured.
281
+
282
+ ### reply
134
283
 
135
284
  ```ruby
136
- robot.to_h # => Hash
285
+ robot.reply(message, "Here's my response")
286
+ # => RobotMessage
137
287
  ```
138
288
 
139
- Returns hash representation of the robot.
289
+ Convenience method that wraps `send_reply`. Extracts the `from` and `key` from the incoming `RobotMessage` automatically.
290
+
291
+ **Parameters:**
292
+
293
+ | Name | Type | Description |
294
+ |------|------|-------------|
295
+ | `message` | `RobotMessage` | The message being replied to |
296
+ | `content` | `String`, `Hash` | Reply payload |
140
297
 
141
- ## Builder DSL
298
+ **Returns:** `RobotMessage`
142
299
 
143
- ### name
300
+ ### on_message
144
301
 
145
302
  ```ruby
146
- name "my_robot"
303
+ robot.on_message { |message| puts message.content }
304
+ # => self
147
305
  ```
148
306
 
149
- Set the robot's name.
307
+ Register a custom handler for incoming bus messages. Block arity controls delivery handling:
308
+
309
+ - **1 argument** `|message|` — auto-acknowledges the delivery before calling the block
310
+ - **2 arguments** `|delivery, message|` — manual mode; you call `delivery.ack!` or `delivery.nack!`
311
+
312
+ **Examples:**
150
313
 
151
- ### description
314
+ ```ruby
315
+ # Auto-ack mode (1 arg)
316
+ robot.on_message do |message|
317
+ joke = run(message.content.to_s).last_text_content
318
+ reply(message, joke)
319
+ end
320
+
321
+ # Manual mode (2 args)
322
+ robot.on_message do |delivery, message|
323
+ if message.content.to_s.length > 10
324
+ delivery.ack!
325
+ reply(message, "Got it!")
326
+ else
327
+ delivery.nack!
328
+ end
329
+ end
330
+ ```
331
+
332
+ ### spawn
152
333
 
153
334
  ```ruby
154
- description "Handles customer inquiries"
335
+ child = robot.spawn(
336
+ name: "specialist",
337
+ system_prompt: "You are a specialist."
338
+ )
339
+ # => RobotLab::Robot (connected to same bus)
155
340
  ```
156
341
 
157
- Set the robot's description.
342
+ Create a new robot on the same message bus. If the parent has no bus, one is created automatically and the parent is connected to it.
158
343
 
159
- ### model
344
+ **Parameters:**
345
+
346
+ | Name | Type | Default | Description |
347
+ |------|------|---------|-------------|
348
+ | `name` | `String` | `"robot"` | Name for the new robot |
349
+ | `system_prompt` | `String`, `nil` | `nil` | Inline system prompt |
350
+ | `template` | `Symbol`, `nil` | `nil` | Prompt template |
351
+ | `local_tools` | `Array` | `[]` | Tools for the new robot |
352
+ | `**options` | `Hash` | `{}` | Additional options passed to `RobotLab.build` |
353
+
354
+ **Returns:** `Robot`
355
+
356
+ **Examples:**
160
357
 
161
358
  ```ruby
162
- model "claude-sonnet-4"
359
+ # Minimal spawn (bus created automatically)
360
+ bot = RobotLab.build
361
+ bot2 = bot.spawn(system_prompt: "You are helpful.")
362
+
363
+ # Spawn with template
364
+ specialist = dispatcher.spawn(
365
+ name: "billing",
366
+ template: :billing,
367
+ local_tools: [InvoiceLookup]
368
+ )
369
+
370
+ # Fan-out: multiple robots with the same name
371
+ worker1 = bot.spawn(name: "worker", system_prompt: "Worker 1")
372
+ worker2 = bot.spawn(name: "worker", system_prompt: "Worker 2")
373
+ # Messages sent to :worker are delivered to both
163
374
  ```
164
375
 
165
- Set the LLM model.
376
+ ### with_bus
166
377
 
167
- ### template
378
+ ```ruby
379
+ robot.with_bus(bus)
380
+ # => self
381
+ ```
382
+
383
+ Connect the robot to a message bus after creation. If called without an argument and the robot has no bus, a new one is created. Returns `self` for chaining.
384
+
385
+ **Parameters:**
386
+
387
+ | Name | Type | Default | Description |
388
+ |------|------|---------|-------------|
389
+ | `bus` | `TypedBus::MessageBus`, `nil` | `nil` | Bus to join (creates one if nil and robot has no bus) |
390
+
391
+ **Returns:** `self`
392
+
393
+ **Examples:**
168
394
 
169
395
  ```ruby
170
- # Inline template
171
- template "You are a helpful assistant."
396
+ # Join an existing bus
397
+ bot = RobotLab.build(name: "bot")
398
+ bot.with_bus(some_bus)
172
399
 
173
- # Template file (loads from template_path)
174
- template "support/system_prompt"
400
+ # Create a bus on demand
401
+ bot = RobotLab.build(name: "bot").with_bus
175
402
 
176
- # Template file with variables
177
- template "support/system_prompt", company: "Acme"
403
+ # Switch buses
404
+ bot.with_bus(bus1) # joins bus1
405
+ bot.with_bus(bus2) # leaves bus1, joins bus2
178
406
  ```
179
407
 
180
- Set the system prompt.
408
+ ### disconnect
181
409
 
182
- ### tool
410
+ ```ruby
411
+ robot.disconnect
412
+ # => self
413
+ ```
414
+
415
+ Disconnect from all MCP servers and bus channels.
416
+
417
+ ### to_h
183
418
 
184
419
  ```ruby
185
- tool :tool_name do
186
- description "What the tool does"
187
- parameter :param, type: :string, required: true
188
- handler { |param:, **_| do_something(param) }
189
- end
420
+ robot.to_h
421
+ # => Hash
190
422
  ```
191
423
 
192
- Define a tool for the robot.
424
+ Returns a hash representation of the robot including name, description, template, system_prompt, local_tools, mcp_tools, mcp_config, tools_config, mcp_servers, model, and bus (true if configured, omitted otherwise).
425
+
426
+ ## Memory Behavior
193
427
 
194
- ### mcp
428
+ - **Standalone**: Robot uses its own inherent `Memory` instance (`robot.memory`).
429
+ - **In a Network**: Robot uses the network's shared memory (passed via `network_memory:`).
195
430
 
196
431
  ```ruby
197
- mcp :inherit # Use network's MCP servers
198
- mcp :none # No MCP servers
199
- mcp [ # Specific servers
200
- { name: "fs", transport: { type: "stdio", command: "mcp-fs" } }
201
- ]
432
+ # Standalone memory access
433
+ robot.memory[:user_id] = 123
434
+ robot.memory[:user_id] # => 123
435
+
436
+ # Reset standalone memory
437
+ robot.reset_memory
202
438
  ```
203
439
 
204
- Configure MCP servers.
440
+ ## Templates
205
441
 
206
- ### tools (whitelist)
442
+ Templates are `.md` files with optional YAML front matter, loaded via `prompt_manager`. The `template:` parameter maps to a file path relative to the configured template directory:
207
443
 
208
444
  ```ruby
209
- tools :inherit # Use network's tools
210
- tools :none # No inherited tools
211
- tools %w[read_file write_file] # Only these tools
445
+ # template: :assistant => prompts/assistant.md
446
+ robot = RobotLab.build(name: "bot", template: :assistant, context: { tone: "friendly" })
447
+ ```
448
+
449
+ Front matter supports two categories of keys:
450
+
451
+ **LLM Config:** `model`, `temperature`, `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop` — applied to the underlying chat.
452
+
453
+ **Robot Extras:** `robot_name`, `description`, `tools`, `mcp` — applied to the robot's identity and capabilities. Constructor-provided values always take precedence.
454
+
455
+ | Key | Type | Description |
456
+ |-----|------|-------------|
457
+ | `robot_name` | `String` | Override robot name (when constructor uses the default `"robot"`) |
458
+ | `description` | `String` | Human-readable description |
459
+ | `tools` | `Array<String>` | Tool class names resolved via `Object.const_get` |
460
+ | `mcp` | `Array<Hash>` | MCP server configurations |
461
+
462
+ ## Configuration Hierarchy
463
+
464
+ Tools and MCP servers use hierarchical resolution: **runtime > robot > network > global config**.
465
+
212
466
  ```
467
+ RobotLab.config (global)
468
+ |
469
+ +-- Network
470
+ | |
471
+ | +-- Robot (build-time mcp:, tools:)
472
+ | |
473
+ | +-- run() call (runtime mcp:, tools:)
474
+ ```
475
+
476
+ Values at each level:
213
477
 
214
- Configure tool whitelist.
478
+ - `:none` -- no tools/MCP at this level
479
+ - `:inherit` -- inherit from parent level
480
+ - `Array` -- explicit list of tool names or MCP server configs
215
481
 
216
482
  ## Examples
217
483
 
218
484
  ### Basic Robot
219
485
 
220
486
  ```ruby
221
- robot = RobotLab.build do
222
- name "greeter"
223
- template "You greet users warmly."
224
- end
487
+ robot = RobotLab.build(
488
+ name: "greeter",
489
+ system_prompt: "You greet users warmly."
490
+ )
491
+ result = robot.run("Hello!")
492
+ puts result.last_text_content
225
493
  ```
226
494
 
227
- ### Robot with Tools
495
+ ### Robot with Template
228
496
 
229
497
  ```ruby
230
- robot = RobotLab.build do
231
- name "calculator"
232
- model "claude-sonnet-4"
233
- template "You help with math problems."
234
-
235
- tool :add do
236
- description "Add two numbers"
237
- parameter :a, type: :number, required: true
238
- parameter :b, type: :number, required: true
239
- handler { |a:, b:, **_| a + b }
240
- end
498
+ robot = RobotLab.build(
499
+ name: "support",
500
+ template: :support,
501
+ context: { company: "Acme Corp" }
502
+ )
503
+ result = robot.run("I need help with my order")
504
+ ```
505
+
506
+ ### Robot with Tools
241
507
 
242
- tool :multiply do
243
- description "Multiply two numbers"
244
- parameter :a, type: :number, required: true
245
- parameter :b, type: :number, required: true
246
- handler { |a:, b:, **_| a * b }
508
+ ```ruby
509
+ class Calculator < RubyLLM::Tool
510
+ description "Performs basic arithmetic"
511
+ param :operation, type: "string", desc: "add, subtract, multiply, divide"
512
+ param :a, type: "number", desc: "First operand"
513
+ param :b, type: "number", desc: "Second operand"
514
+
515
+ def execute(operation:, a:, b:)
516
+ case operation
517
+ when "add" then a + b
518
+ when "subtract" then a - b
519
+ when "multiply" then a * b
520
+ when "divide" then a.to_f / b
521
+ end
247
522
  end
248
523
  end
524
+
525
+ robot = RobotLab.build(
526
+ name: "math_bot",
527
+ system_prompt: "You help with math.",
528
+ local_tools: [Calculator]
529
+ )
530
+ result = robot.run("What is 15 * 7?")
249
531
  ```
250
532
 
251
533
  ### Robot with MCP
252
534
 
253
535
  ```ruby
254
- robot = RobotLab.build do
255
- name "developer"
256
- template "You help with coding tasks."
257
-
258
- mcp [
536
+ robot = RobotLab.build(
537
+ name: "developer",
538
+ system_prompt: "You help with coding tasks.",
539
+ mcp: [
259
540
  {
260
541
  name: "github",
261
- transport: { type: "stdio", command: "mcp-server-github" }
542
+ transport: { type: "stdio", command: "github-mcp-server", args: ["stdio"] }
262
543
  }
263
544
  ]
545
+ )
546
+ result = robot.run("Search for popular Ruby repos")
547
+ robot.disconnect
548
+ ```
549
+
550
+ ### Bare Robot with Chaining
551
+
552
+ ```ruby
553
+ robot = RobotLab.build(name: "bot")
554
+ result = robot
555
+ .with_instructions("Be concise.")
556
+ .with_temperature(0.3)
557
+ .run("Explain quantum computing")
558
+ ```
559
+
560
+ ### Robot with Message Bus
561
+
562
+ ```ruby
563
+ bus = TypedBus::MessageBus.new
564
+
565
+ bob = RobotLab.build(name: "bob", system_prompt: "You tell jokes.", bus: bus)
566
+
567
+ alice = RobotLab.build(name: "alice", system_prompt: "You evaluate jokes.", bus: bus)
568
+ alice.on_message do |message|
569
+ verdict = alice.run("Is this funny? #{message.content}").last_text_content
570
+ puts verdict
571
+ end
264
572
 
265
- tools %w[search_repositories create_issue]
573
+ bob.on_message do |message|
574
+ joke = bob.run(message.content.to_s).last_text_content
575
+ bob.reply(message, joke)
266
576
  end
577
+
578
+ alice.send_message(to: :bob, content: "Tell me a robot joke.")
579
+ ```
580
+
581
+ ### Spawning Robots Dynamically
582
+
583
+ ```ruby
584
+ # Parent robot spawns specialists on demand
585
+ dispatcher = RobotLab.build(
586
+ name: "dispatcher",
587
+ system_prompt: "You delegate work."
588
+ )
589
+
590
+ dispatcher.on_message do |message|
591
+ puts "Reply from #{message.from}: #{message.content}"
592
+ end
593
+
594
+ # spawn creates child on same bus (bus created lazily)
595
+ helper = dispatcher.spawn(
596
+ name: "helper",
597
+ system_prompt: "You answer questions concisely."
598
+ )
599
+
600
+ answer = helper.run("What is 2+2?").last_text_content
601
+ helper.send_message(to: :dispatcher, content: answer)
602
+ ```
603
+
604
+ ### Connecting to a Bus After Creation
605
+
606
+ ```ruby
607
+ bot = RobotLab.build(name: "latecomer", system_prompt: "Hi there.")
608
+
609
+ # Join a bus later
610
+ bus = TypedBus::MessageBus.new
611
+ bot.with_bus(bus)
612
+
613
+ # Now bot can send/receive messages
614
+ bot.send_message(to: :someone, content: "Hello!")
267
615
  ```
268
616
 
269
617
  ## See Also