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
data/docs/concepts.md CHANGED
@@ -4,27 +4,74 @@ Understanding the fundamental concepts in RobotLab will help you build effective
4
4
 
5
5
  ## Robot
6
6
 
7
- A **Robot** is an LLM-powered agent with a specific personality, capabilities, and tools. Each robot has:
7
+ A **Robot** is an LLM-powered agent that inherits from `RubyLLM::Agent`. Each robot wraps a persistent chat session created at initialization and provides template-based prompts, tools, memory, and MCP integration. Robots are created using keyword arguments via the `RobotLab.build` factory method.
8
8
 
9
- - **Name**: A unique identifier within a network
10
- - **Description**: What the robot does (used for routing decisions)
11
- - **Template/System Prompt**: Instructions that define the robot's behavior
12
- - **Model**: The LLM model to use (e.g., `claude-sonnet-4`)
13
- - **Tools**: Custom functions the robot can call
9
+ Each robot has:
10
+
11
+ - **Name**: A unique identifier (auto-generated if omitted)
12
+ - **Template**: A `.md` file with YAML front matter managed by prompt_manager, referenced by symbol
13
+ - **System Prompt**: Inline instructions (can be used alone or combined with a template)
14
+ - **Model**: The LLM model to use (defaults to `RobotLab.config.ruby_llm.model`)
15
+ - **Local Tools**: `RubyLLM::Tool` subclasses or `RobotLab::Tool` instances
16
+ - **Memory**: Persistent key-value store across runs
14
17
 
15
18
  ```ruby
16
- robot = RobotLab.build do
17
- name "support_agent"
18
- description "Handles customer support inquiries"
19
- model "claude-sonnet-4"
20
- template "You are a friendly customer support agent..."
19
+ # Robot with template (references prompts/support.md)
20
+ robot = RobotLab.build(
21
+ name: "support_agent",
22
+ template: :support,
23
+ context: { tone: "friendly", department: "billing" },
24
+ local_tools: [OrderLookup, RefundProcessor],
25
+ model: "claude-sonnet-4"
26
+ )
21
27
 
22
- tool :lookup_order do
23
- description "Look up order details by order ID"
24
- parameter :order_id, type: :string, required: true
25
- handler { |order_id:| Order.find(order_id).to_h }
26
- end
27
- end
28
+ # Robot with inline system prompt
29
+ robot = RobotLab.build(
30
+ name: "helper",
31
+ system_prompt: "You are a friendly customer support agent."
32
+ )
33
+
34
+ # Bare robot configured via chaining
35
+ robot = RobotLab.build(name: "bot")
36
+ robot.with_instructions("Be concise.").with_temperature(0.3).run("Hello")
37
+ ```
38
+
39
+ The primary method is `robot.run("message")`, which takes a positional string argument and returns a `RobotResult`:
40
+
41
+ ```ruby
42
+ result = robot.run("What is 2 + 2?")
43
+ puts result.last_text_content # => "4"
44
+ ```
45
+
46
+ Standalone robots persist their conversation history and memory across runs:
47
+
48
+ ```ruby
49
+ robot.run("My name is Alice.")
50
+ result = robot.run("What is my name?")
51
+ puts result.last_text_content # => "Your name is Alice."
52
+ ```
53
+
54
+ ## Configuration
55
+
56
+ RobotLab uses `MywayConfig` for configuration. There is no `RobotLab.configure` block. Instead, configuration is loaded automatically from multiple sources in priority order:
57
+
58
+ 1. Bundled defaults (`lib/robot_lab/config/defaults.yml`)
59
+ 2. Environment-specific overrides (development, test, production)
60
+ 3. XDG user config (`~/.config/robot_lab/config.yml`)
61
+ 4. Project config (`./config/robot_lab.yml`)
62
+ 5. Environment variables (`ROBOT_LAB_*` prefix)
63
+
64
+ ```ruby
65
+ # Access configuration values
66
+ RobotLab.config.ruby_llm.model #=> "claude-sonnet-4"
67
+ RobotLab.config.ruby_llm.request_timeout #=> 120
68
+
69
+ # Set API keys via environment variables
70
+ # ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...
71
+ # ROBOT_LAB_RUBY_LLM__OPENAI_API_KEY=sk-...
72
+
73
+ # Reload configuration
74
+ RobotLab.reload_config!
28
75
  ```
29
76
 
30
77
  ## Network
@@ -35,6 +82,7 @@ A **Network** is a collection of robots orchestrated using [SimpleFlow](https://
35
82
  - **Parallel Execution**: Tasks with the same dependencies run concurrently
36
83
  - **Optional Task Activation**: Dynamic routing based on robot output
37
84
  - **Per-Task Configuration**: Each task can have its own context, tools, and MCP servers
85
+ - **Shared Memory**: All robots in a network share a reactive memory instance
38
86
 
39
87
  ```ruby
40
88
  network = RobotLab.create_network(name: "customer_service") do
@@ -46,6 +94,8 @@ network = RobotLab.create_network(name: "customer_service") do
46
94
  context: { department: "technical" },
47
95
  depends_on: :optional
48
96
  end
97
+
98
+ result = network.run(message: "I was charged twice for my subscription.")
49
99
  ```
50
100
 
51
101
  ## Task
@@ -53,8 +103,8 @@ end
53
103
  A **Task** wraps a robot for use in a network pipeline with per-task configuration:
54
104
 
55
105
  - **Context**: Task-specific context deep-merged with network run params
56
- - **MCP**: MCP servers available to this task
57
- - **Tools**: Tools available to this task
106
+ - **MCP**: MCP servers available to this task (`:none`, `:inherit`, or array)
107
+ - **Tools**: Tools available to this task (`:none`, `:inherit`, or array)
58
108
  - **Memory**: Task-specific memory
59
109
  - **Dependencies**: `:none`, `[:task1, :task2]`, or `:optional`
60
110
 
@@ -87,53 +137,123 @@ result.continued? # Whether execution continues
87
137
 
88
138
  ## Tool
89
139
 
90
- **Tools** are functions that robots can call to interact with external systems:
140
+ **Tools** give robots the ability to interact with external systems. There are two patterns for defining tools:
141
+
142
+ ### RubyLLM::Tool Subclass (Preferred)
143
+
144
+ ```ruby
145
+ class Calculator < RubyLLM::Tool
146
+ description "Performs basic arithmetic operations"
147
+
148
+ param :operation, type: "string", desc: "The operation (add, subtract, multiply, divide)"
149
+ param :a, type: "number", desc: "First operand"
150
+ param :b, type: "number", desc: "Second operand"
151
+
152
+ def execute(operation:, a:, b:)
153
+ case operation
154
+ when "add" then a + b
155
+ when "subtract" then a - b
156
+ when "multiply" then a * b
157
+ when "divide" then a.to_f / b
158
+ else "Unknown operation: #{operation}"
159
+ end
160
+ end
161
+ end
162
+
163
+ robot = RobotLab.build(
164
+ name: "math_bot",
165
+ system_prompt: "You can do math.",
166
+ local_tools: [Calculator]
167
+ )
168
+ ```
169
+
170
+ ### RobotLab::Tool with Block Handler
91
171
 
92
172
  ```ruby
93
173
  tool = RobotLab::Tool.new(
94
174
  name: "get_weather",
95
175
  description: "Get current weather for a location",
96
176
  parameters: {
97
- location: { type: "string", description: "City name" }
98
- },
99
- handler: ->(location:, **_context) {
100
- WeatherService.current(location)
177
+ type: "object",
178
+ properties: {
179
+ location: { type: "string", description: "City name" }
180
+ },
181
+ required: ["location"]
101
182
  }
102
- )
183
+ ) do |input, **_opts|
184
+ WeatherService.current(input[:location])
185
+ end
103
186
  ```
104
187
 
105
- ## Message Types
188
+ ## RobotResult
106
189
 
107
- RobotLab uses several message types to represent conversation content:
190
+ `RobotResult` captures the output of a single `robot.run(...)` call:
108
191
 
109
- | Type | Purpose |
110
- |------|---------|
111
- | `TextMessage` | User or assistant text content |
112
- | `ToolMessage` | Tool definition with name and parameters |
113
- | `ToolCallMessage` | Request from LLM to execute a tool |
114
- | `ToolResultMessage` | Result returned from tool execution |
192
+ ```ruby
193
+ result = robot.run("Hello!")
194
+
195
+ result.last_text_content # => "Hi there!" (String or nil)
196
+ result.output # => [TextMessage, ...] array of output messages
197
+ result.tool_calls # => [] array of tool call results
198
+ result.robot_name # => "assistant"
199
+ result.stop_reason # => "end_turn" or nil
200
+ result.has_tool_calls? # => false
201
+ result.checksum # => "a1b2c3d4..." (for dedup)
202
+ ```
115
203
 
116
204
  ## Memory
117
205
 
118
- **Memory** provides persistent storage across robot executions:
206
+ **Memory** is a reactive key-value store that provides persistent storage across robot executions. Standalone robots use their own inherent memory; robots in a network share the network's memory.
119
207
 
120
208
  ```ruby
121
- # Robot with inherent memory
209
+ # Standalone robot with inherent memory
122
210
  robot = RobotLab.build(name: "assistant", system_prompt: "You are helpful.")
123
- robot.run(message: "My name is Alice")
124
- robot.run(message: "What's my name?") # Memory persists
211
+ robot.run("My name is Alice")
212
+ robot.run("What's my name?") # Memory persists across runs
125
213
 
126
- # Access robot's memory
214
+ # Access robot's memory directly
127
215
  robot.memory[:user_id] = 123
128
216
  robot.memory.data[:category] = "billing"
217
+ robot.memory.data.category # => "billing" (method-style access)
129
218
 
130
219
  # Runtime memory injection
131
- robot.run(message: "Help me", memory: { session_id: "abc123" })
220
+ robot.run("Help me", memory: { session_id: "abc123" })
132
221
 
133
222
  # Reset memory
134
223
  robot.reset_memory
135
224
  ```
136
225
 
226
+ ### Reserved Memory Keys
227
+
228
+ | Key | Purpose |
229
+ |-----|---------|
230
+ | `:data` | Runtime data (StateProxy for method-style access) |
231
+ | `:results` | Accumulated robot results |
232
+ | `:messages` | Conversation history |
233
+ | `:session_id` | Session identifier for history persistence |
234
+ | `:cache` | Semantic cache instance (RubyLLM::SemanticCache) |
235
+
236
+ ### Reactive Memory in Networks
237
+
238
+ In a network, shared memory supports pub/sub semantics for inter-robot communication:
239
+
240
+ ```ruby
241
+ # Robot A writes to shared memory
242
+ network.memory.set(:sentiment, { score: 0.8 })
243
+
244
+ # Robot B reads (blocking until available)
245
+ result = network.memory.get(:sentiment, wait: true)
246
+ result = network.memory.get(:sentiment, wait: 30) # timeout in seconds
247
+
248
+ # Multiple keys
249
+ results = network.memory.get(:sentiment, :entities, :keywords, wait: 60)
250
+
251
+ # Subscribe to changes
252
+ network.memory.subscribe(:status) do |change|
253
+ puts "#{change.key} changed by #{change.writer}: #{change.value}"
254
+ end
255
+ ```
256
+
137
257
  ## MCP (Model Context Protocol)
138
258
 
139
259
  **MCP** allows robots to connect to external tool servers:
@@ -141,6 +261,7 @@ robot.reset_memory
141
261
  ```ruby
142
262
  robot = RobotLab.build(
143
263
  name: "developer",
264
+ system_prompt: "You are a developer assistant.",
144
265
  mcp: [
145
266
  { name: "filesystem", transport: { type: "stdio", command: "mcp-server-filesystem" } },
146
267
  { name: "github", transport: { type: "stdio", command: "mcp-server-github" } }
@@ -148,6 +269,8 @@ robot = RobotLab.build(
148
269
  )
149
270
  ```
150
271
 
272
+ MCP configuration follows a hierarchical resolution: `runtime > robot > network > global config`. Values can be `:none`, `:inherit`, or explicit arrays.
273
+
151
274
  ## Execution Flow
152
275
 
153
276
  ```mermaid
@@ -160,11 +283,12 @@ sequenceDiagram
160
283
  participant LLM
161
284
  participant Tool
162
285
 
163
- User->>Network: run(message, context)
164
- Network->>Pipeline: call(initial_result)
286
+ User->>Network: run(message: "...", **context)
287
+ Network->>Pipeline: call_parallel(initial_result)
165
288
  Pipeline->>Task: call(result)
166
289
  Task->>Robot: call(enhanced_result)
167
- Robot->>LLM: inference(messages, tools)
290
+ Robot->>Robot: extract_run_context(result)
291
+ Robot->>LLM: ask(message)
168
292
 
169
293
  alt Tool Call
170
294
  LLM-->>Robot: tool_call
@@ -175,7 +299,7 @@ sequenceDiagram
175
299
 
176
300
  LLM-->>Robot: response
177
301
  Robot-->>Task: RobotResult
178
- Task-->>Pipeline: result.continue(value)
302
+ Task-->>Pipeline: result.continue(robot_result)
179
303
 
180
304
  alt Optional Task Activated
181
305
  Pipeline->>Task: call activated task
@@ -185,21 +309,26 @@ sequenceDiagram
185
309
  Network-->>User: SimpleFlow::Result
186
310
  ```
187
311
 
188
- ## Conditional Routing
312
+ ## Conditional Routing with ClassifierRobot
189
313
 
190
- Use custom Robot subclasses to implement intelligent routing:
314
+ Use a custom Robot subclass to implement intelligent routing. Override `call(result)` to inspect the LLM output and activate optional tasks:
191
315
 
192
316
  ```ruby
193
317
  class ClassifierRobot < RobotLab::Robot
194
318
  def call(result)
195
- robot_result = run(**extract_run_context(result))
319
+ # Extract context and message from the pipeline result
320
+ context = extract_run_context(result)
321
+ message = context.delete(:message)
322
+
323
+ # Run the robot to classify the input
324
+ robot_result = run(message, **context)
196
325
 
197
326
  new_result = result
198
327
  .with_context(@name.to_sym, robot_result)
199
328
  .continue(robot_result)
200
329
 
201
- # Activate appropriate specialist
202
- category = robot_result.last_text_content.to_s.downcase
330
+ # Activate the appropriate specialist based on classification
331
+ category = robot_result.last_text_content.to_s.strip.downcase
203
332
  case category
204
333
  when /billing/ then new_result.activate(:billing)
205
334
  when /technical/ then new_result.activate(:technical)
@@ -207,8 +336,143 @@ class ClassifierRobot < RobotLab::Robot
207
336
  end
208
337
  end
209
338
  end
339
+
340
+ # Build the classifier (uses a .md template with YAML front matter)
341
+ classifier = ClassifierRobot.new(
342
+ name: "classifier",
343
+ template: :classifier,
344
+ model: "claude-sonnet-4"
345
+ )
346
+
347
+ # Build specialist robots
348
+ billing_robot = RobotLab.build(name: "billing", template: :billing)
349
+ technical_robot = RobotLab.build(name: "technical", template: :technical)
350
+ general_robot = RobotLab.build(name: "general", template: :general)
351
+
352
+ # Create network with optional task routing
353
+ network = RobotLab.create_network(name: "support_network") do
354
+ task :classifier, classifier, depends_on: :none
355
+ task :billing, billing_robot, depends_on: :optional
356
+ task :technical, technical_robot, depends_on: :optional
357
+ task :general, general_robot, depends_on: :optional
358
+ end
359
+
360
+ result = network.run(message: "I was charged twice for my subscription.")
361
+ puts result.value.last_text_content
362
+ ```
363
+
364
+ ## Message Bus
365
+
366
+ The **Message Bus** enables bidirectional, cyclic communication between robots via `typed_bus`. While Networks enforce DAG-based execution, the bus supports negotiation loops, convergence patterns, and multi-turn dialogues.
367
+
368
+ ```ruby
369
+ bus = TypedBus::MessageBus.new
370
+
371
+ bob = RobotLab.build(name: "bob", system_prompt: "You tell jokes.", bus: bus)
372
+ alice = RobotLab.build(name: "alice", system_prompt: "You evaluate jokes.", bus: bus)
373
+
374
+ # Register handlers
375
+ bob.on_message do |message|
376
+ joke = bob.run(message.content.to_s).last_text_content
377
+ bob.reply(message, joke)
378
+ end
379
+
380
+ alice.on_message do |message|
381
+ verdict = alice.run("Is this funny? #{message.content}").last_text_content
382
+ # Send another request if not satisfied
383
+ alice.send_message(to: :bob, content: "Try again.") unless verdict.start_with?("FUNNY")
384
+ end
385
+
386
+ # Start the conversation
387
+ alice.send_message(to: :bob, content: "Tell me a robot joke.")
210
388
  ```
211
389
 
390
+ Key features:
391
+
392
+ - **Typed channels** — only `RobotMessage` objects accepted per channel
393
+ - **Auto-ACK** — 1-arg `on_message` blocks auto-acknowledge; 2-arg blocks give manual control
394
+ - **Reply correlation** — `reply(message, content)` tracks threads via `in_reply_to`
395
+ - **Independent of Network** — bus works without a Network pipeline
396
+
397
+ ### Dynamic Spawning
398
+
399
+ Robots can create new robots at runtime using `spawn`. The bus is created lazily:
400
+
401
+ ```ruby
402
+ dispatcher = RobotLab.build(name: "dispatcher", system_prompt: "You delegate work.")
403
+
404
+ # spawn creates a child on the same bus (bus created automatically)
405
+ helper = dispatcher.spawn(name: "helper", system_prompt: "You answer questions.")
406
+ answer = helper.run("What is 2+2?").last_text_content
407
+ helper.send_message(to: :dispatcher, content: answer)
408
+ ```
409
+
410
+ Robots can also join a bus after creation with `with_bus`:
411
+
412
+ ```ruby
413
+ bot = RobotLab.build(name: "latecomer", system_prompt: "Hello.")
414
+ bot.with_bus(existing_bus)
415
+ ```
416
+
417
+ Multiple robots with the same name enable fan-out — messages sent to that name are delivered to all subscribers.
418
+
419
+ ## Templates
420
+
421
+ Templates are `.md` files with YAML front matter, managed by the prompt_manager gem. They live in the configured template path (default: `./prompts/` or `app/prompts/` in Rails).
422
+
423
+ ```markdown
424
+ ---
425
+ description: Customer support classifier
426
+ model: claude-sonnet-4
427
+ temperature: 0.3
428
+ ---
429
+ You are a request classifier. Analyze the user's request and classify it
430
+ as either "billing", "technical", or "general".
431
+
432
+ Respond with ONLY the category name, nothing else.
433
+ ```
434
+
435
+ Reference templates by symbol when building robots:
436
+
437
+ ```ruby
438
+ robot = RobotLab.build(
439
+ name: "classifier",
440
+ template: :classifier, # loads prompts/classifier.md
441
+ context: { tone: "professional" } # variables passed to the template
442
+ )
443
+ ```
444
+
445
+ ### Front Matter Keys
446
+
447
+ Templates support two categories of front matter keys:
448
+
449
+ **LLM Config:** `model`, `temperature`, `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop` — applied to the robot's chat configuration.
450
+
451
+ **Robot Extras:** `robot_name`, `description`, `tools`, `mcp` — applied to the robot's identity and capabilities. These make templates self-contained: reading the `.md` file tells you everything about the robot.
452
+
453
+ ```markdown
454
+ ---
455
+ description: GitHub assistant with MCP tool access
456
+ robot_name: github_bot
457
+ tools:
458
+ - CodeSearchTool
459
+ mcp:
460
+ - name: github
461
+ transport: stdio
462
+ command: npx
463
+ args: ["-y", "@modelcontextprotocol/server-github"]
464
+ model: claude-sonnet-4
465
+ ---
466
+ You are a GitHub assistant. Use available tools to help with repository tasks.
467
+ ```
468
+
469
+ ```ruby
470
+ # Template provides everything — minimal constructor
471
+ robot = RobotLab.build(template: :github_assistant)
472
+ ```
473
+
474
+ Constructor-provided values (`local_tools:`, `mcp:`, `name:`, `description:`) always take precedence over front matter values.
475
+
212
476
  ## Next Steps
213
477
 
214
478
  - [Quick Start Guide](getting-started/quick-start.md) - Build your first robot