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.
Files changed (187) 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 +140 -0
  5. data/README.md +263 -48
  6. data/Rakefile +71 -1
  7. data/docs/api/core/index.md +53 -46
  8. data/docs/api/core/memory.md +200 -154
  9. data/docs/api/core/network.md +13 -3
  10. data/docs/api/core/robot.md +490 -130
  11. data/docs/api/core/state.md +55 -73
  12. data/docs/api/core/tool.md +205 -209
  13. data/docs/api/index.md +7 -28
  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/messages/index.md +35 -20
  19. data/docs/api/messages/text-message.md +67 -21
  20. data/docs/api/messages/tool-call-message.md +80 -41
  21. data/docs/api/messages/tool-result-message.md +119 -50
  22. data/docs/api/messages/user-message.md +48 -24
  23. data/docs/api/streaming/context.md +157 -74
  24. data/docs/api/streaming/events.md +114 -166
  25. data/docs/api/streaming/index.md +74 -72
  26. data/docs/architecture/core-concepts.md +360 -116
  27. data/docs/architecture/index.md +97 -59
  28. data/docs/architecture/message-flow.md +138 -129
  29. data/docs/architecture/network-orchestration.md +197 -50
  30. data/docs/architecture/robot-execution.md +199 -146
  31. data/docs/architecture/state-management.md +255 -187
  32. data/docs/concepts.md +311 -49
  33. data/docs/examples/basic-chat.md +89 -77
  34. data/docs/examples/index.md +222 -47
  35. data/docs/examples/mcp-server.md +207 -203
  36. data/docs/examples/multi-robot-network.md +129 -35
  37. data/docs/examples/rails-application.md +159 -160
  38. data/docs/examples/tool-usage.md +295 -204
  39. data/docs/getting-started/configuration.md +347 -154
  40. data/docs/getting-started/index.md +1 -1
  41. data/docs/getting-started/installation.md +22 -13
  42. data/docs/getting-started/quick-start.md +166 -121
  43. data/docs/guides/building-robots.md +418 -212
  44. data/docs/guides/creating-networks.md +143 -24
  45. data/docs/guides/index.md +0 -5
  46. data/docs/guides/mcp-integration.md +152 -113
  47. data/docs/guides/memory.md +220 -164
  48. data/docs/guides/rails-integration.md +244 -162
  49. data/docs/guides/streaming.md +137 -187
  50. data/docs/guides/using-tools.md +259 -212
  51. data/docs/index.md +46 -41
  52. data/examples/01_simple_robot.rb +6 -9
  53. data/examples/02_tools.rb +6 -9
  54. data/examples/03_network.rb +19 -17
  55. data/examples/04_mcp.rb +5 -8
  56. data/examples/05_streaming.rb +5 -8
  57. data/examples/06_prompt_templates.rb +42 -37
  58. data/examples/07_network_memory.rb +13 -14
  59. data/examples/08_llm_config.rb +169 -0
  60. data/examples/09_chaining.rb +262 -0
  61. data/examples/10_memory.rb +331 -0
  62. data/examples/11_network_introspection.rb +253 -0
  63. data/examples/12_message_bus.rb +74 -0
  64. data/examples/13_spawn.rb +90 -0
  65. data/examples/14_rusty_circuit/comic.rb +143 -0
  66. data/examples/14_rusty_circuit/display.rb +203 -0
  67. data/examples/14_rusty_circuit/heckler.rb +63 -0
  68. data/examples/14_rusty_circuit/open_mic.rb +123 -0
  69. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +20 -0
  70. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +23 -0
  71. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +20 -0
  72. data/examples/14_rusty_circuit/scout.rb +156 -0
  73. data/examples/14_rusty_circuit/scout_notes.md +89 -0
  74. data/examples/14_rusty_circuit/show.log +234 -0
  75. data/examples/15_memory_network_and_bus/editor_in_chief.rb +24 -0
  76. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +206 -0
  77. data/examples/15_memory_network_and_bus/linux_writer.rb +80 -0
  78. data/examples/15_memory_network_and_bus/os_editor.rb +46 -0
  79. data/examples/15_memory_network_and_bus/os_writer.rb +46 -0
  80. data/examples/15_memory_network_and_bus/output/combined_article.md +13 -0
  81. data/examples/15_memory_network_and_bus/output/final_article.md +15 -0
  82. data/examples/15_memory_network_and_bus/output/linux_draft.md +5 -0
  83. data/examples/15_memory_network_and_bus/output/mac_draft.md +7 -0
  84. data/examples/15_memory_network_and_bus/output/memory.json +13 -0
  85. data/examples/15_memory_network_and_bus/output/revision_1.md +19 -0
  86. data/examples/15_memory_network_and_bus/output/revision_2.md +15 -0
  87. data/examples/15_memory_network_and_bus/output/windows_draft.md +7 -0
  88. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +13 -0
  89. data/examples/15_memory_network_and_bus/prompts/os_chief.md +13 -0
  90. data/examples/15_memory_network_and_bus/prompts/os_editor.md +13 -0
  91. data/examples/16_writers_room/display.rb +158 -0
  92. data/examples/16_writers_room/output/.gitignore +2 -0
  93. data/examples/16_writers_room/output/opus_001.md +263 -0
  94. data/examples/16_writers_room/output/opus_001_notes.log +470 -0
  95. data/examples/16_writers_room/prompts/writer.md +37 -0
  96. data/examples/16_writers_room/room.rb +150 -0
  97. data/examples/16_writers_room/tools.rb +162 -0
  98. data/examples/16_writers_room/writer.rb +121 -0
  99. data/examples/16_writers_room/writers_room.rb +162 -0
  100. data/examples/README.md +197 -0
  101. data/examples/prompts/{assistant/system.txt.erb → assistant.md} +3 -0
  102. data/examples/prompts/{billing/system.txt.erb → billing.md} +3 -0
  103. data/examples/prompts/{classifier/system.txt.erb → classifier.md} +3 -0
  104. data/examples/prompts/comedian.md +6 -0
  105. data/examples/prompts/comedy_critic.md +10 -0
  106. data/examples/prompts/configurable.md +9 -0
  107. data/examples/prompts/dispatcher.md +12 -0
  108. data/examples/prompts/{entity_extractor/system.txt.erb → entity_extractor.md} +3 -0
  109. data/examples/prompts/{escalation/system.txt.erb → escalation.md} +7 -0
  110. data/examples/prompts/frontmatter_mcp_test.md +9 -0
  111. data/examples/prompts/frontmatter_named_test.md +5 -0
  112. data/examples/prompts/frontmatter_tools_test.md +6 -0
  113. data/examples/prompts/{general/system.txt.erb → general.md} +3 -0
  114. data/examples/prompts/{github_assistant/system.txt.erb → github_assistant.md} +8 -0
  115. data/examples/prompts/{helper/system.txt.erb → helper.md} +3 -0
  116. data/examples/prompts/{keyword_extractor/system.txt.erb → keyword_extractor.md} +3 -0
  117. data/examples/prompts/llm_config_demo.md +20 -0
  118. data/examples/prompts/{order_support/system.txt.erb → order_support.md} +8 -0
  119. data/examples/prompts/os_advocate.md +13 -0
  120. data/examples/prompts/os_chief.md +13 -0
  121. data/examples/prompts/os_editor.md +13 -0
  122. data/examples/prompts/{product_support/system.txt.erb → product_support.md} +7 -0
  123. data/examples/prompts/{sentiment_analyzer/system.txt.erb → sentiment_analyzer.md} +3 -0
  124. data/examples/prompts/{synthesizer/system.txt.erb → synthesizer.md} +3 -0
  125. data/examples/prompts/{technical/system.txt.erb → technical.md} +3 -0
  126. data/examples/prompts/{triage/system.txt.erb → triage.md} +6 -0
  127. data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -13
  128. data/lib/robot_lab/ask_user.rb +75 -0
  129. data/lib/robot_lab/config/defaults.yml +121 -0
  130. data/lib/robot_lab/config.rb +183 -0
  131. data/lib/robot_lab/error.rb +6 -0
  132. data/lib/robot_lab/mcp/client.rb +1 -1
  133. data/lib/robot_lab/memory.rb +10 -34
  134. data/lib/robot_lab/network.rb +13 -20
  135. data/lib/robot_lab/robot/bus_messaging.rb +239 -0
  136. data/lib/robot_lab/robot/mcp_management.rb +88 -0
  137. data/lib/robot_lab/robot/template_rendering.rb +130 -0
  138. data/lib/robot_lab/robot.rb +240 -330
  139. data/lib/robot_lab/robot_message.rb +44 -0
  140. data/lib/robot_lab/robot_result.rb +1 -0
  141. data/lib/robot_lab/run_config.rb +184 -0
  142. data/lib/robot_lab/state_proxy.rb +2 -12
  143. data/lib/robot_lab/streaming/context.rb +1 -1
  144. data/lib/robot_lab/task.rb +8 -1
  145. data/lib/robot_lab/tool.rb +108 -172
  146. data/lib/robot_lab/tool_config.rb +1 -1
  147. data/lib/robot_lab/tool_manifest.rb +2 -18
  148. data/lib/robot_lab/utils.rb +39 -0
  149. data/lib/robot_lab/version.rb +1 -1
  150. data/lib/robot_lab.rb +89 -57
  151. data/mkdocs.yml +0 -11
  152. metadata +121 -135
  153. data/docs/api/adapters/anthropic.md +0 -121
  154. data/docs/api/adapters/gemini.md +0 -133
  155. data/docs/api/adapters/index.md +0 -104
  156. data/docs/api/adapters/openai.md +0 -134
  157. data/docs/api/history/active-record-adapter.md +0 -195
  158. data/docs/api/history/config.md +0 -191
  159. data/docs/api/history/index.md +0 -132
  160. data/docs/api/history/thread-manager.md +0 -144
  161. data/docs/guides/history.md +0 -359
  162. data/examples/prompts/assistant/user.txt.erb +0 -1
  163. data/examples/prompts/billing/user.txt.erb +0 -1
  164. data/examples/prompts/classifier/user.txt.erb +0 -1
  165. data/examples/prompts/entity_extractor/user.txt.erb +0 -3
  166. data/examples/prompts/escalation/user.txt.erb +0 -34
  167. data/examples/prompts/general/user.txt.erb +0 -1
  168. data/examples/prompts/github_assistant/user.txt.erb +0 -1
  169. data/examples/prompts/helper/user.txt.erb +0 -1
  170. data/examples/prompts/keyword_extractor/user.txt.erb +0 -3
  171. data/examples/prompts/order_support/user.txt.erb +0 -22
  172. data/examples/prompts/product_support/user.txt.erb +0 -32
  173. data/examples/prompts/sentiment_analyzer/user.txt.erb +0 -3
  174. data/examples/prompts/synthesizer/user.txt.erb +0 -15
  175. data/examples/prompts/technical/user.txt.erb +0 -1
  176. data/examples/prompts/triage/user.txt.erb +0 -17
  177. data/lib/robot_lab/adapters/anthropic.rb +0 -163
  178. data/lib/robot_lab/adapters/base.rb +0 -85
  179. data/lib/robot_lab/adapters/gemini.rb +0 -193
  180. data/lib/robot_lab/adapters/openai.rb +0 -159
  181. data/lib/robot_lab/adapters/registry.rb +0 -81
  182. data/lib/robot_lab/configuration.rb +0 -143
  183. data/lib/robot_lab/errors.rb +0 -70
  184. data/lib/robot_lab/history/active_record_adapter.rb +0 -146
  185. data/lib/robot_lab/history/config.rb +0 -115
  186. data/lib/robot_lab/history/thread_manager.rb +0 -93
  187. data/lib/robot_lab/robotic_model.rb +0 -324
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,121 @@ 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)
91
143
 
92
144
  ```ruby
93
- tool = RobotLab::Tool.new(
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.create Factory
171
+
172
+ ```ruby
173
+ tool = RobotLab::Tool.create(
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
+ ) { |args| WeatherService.current(args[:location]) }
103
184
  ```
104
185
 
105
- ## Message Types
186
+ ## RobotResult
106
187
 
107
- RobotLab uses several message types to represent conversation content:
188
+ `RobotResult` captures the output of a single `robot.run(...)` call:
108
189
 
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 |
190
+ ```ruby
191
+ result = robot.run("Hello!")
192
+
193
+ result.last_text_content # => "Hi there!" (String or nil)
194
+ result.output # => [TextMessage, ...] array of output messages
195
+ result.tool_calls # => [] array of tool call results
196
+ result.robot_name # => "assistant"
197
+ result.stop_reason # => "end_turn" or nil
198
+ result.has_tool_calls? # => false
199
+ result.checksum # => "a1b2c3d4..." (for dedup)
200
+ ```
115
201
 
116
202
  ## Memory
117
203
 
118
- **Memory** provides persistent storage across robot executions:
204
+ **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
205
 
120
206
  ```ruby
121
- # Robot with inherent memory
207
+ # Standalone robot with inherent memory
122
208
  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
209
+ robot.run("My name is Alice")
210
+ robot.run("What's my name?") # Memory persists across runs
125
211
 
126
- # Access robot's memory
212
+ # Access robot's memory directly
127
213
  robot.memory[:user_id] = 123
128
214
  robot.memory.data[:category] = "billing"
215
+ robot.memory.data.category # => "billing" (method-style access)
129
216
 
130
217
  # Runtime memory injection
131
- robot.run(message: "Help me", memory: { session_id: "abc123" })
218
+ robot.run("Help me", memory: { session_id: "abc123" })
132
219
 
133
220
  # Reset memory
134
221
  robot.reset_memory
135
222
  ```
136
223
 
224
+ ### Reserved Memory Keys
225
+
226
+ | Key | Purpose |
227
+ |-----|---------|
228
+ | `:data` | Runtime data (StateProxy for method-style access) |
229
+ | `:results` | Accumulated robot results |
230
+ | `:messages` | Conversation history |
231
+ | `:session_id` | Session identifier for history persistence |
232
+ | `:cache` | Semantic cache instance (RubyLLM::SemanticCache) |
233
+
234
+ ### Reactive Memory in Networks
235
+
236
+ In a network, shared memory supports pub/sub semantics for inter-robot communication:
237
+
238
+ ```ruby
239
+ # Robot A writes to shared memory
240
+ network.memory.set(:sentiment, { score: 0.8 })
241
+
242
+ # Robot B reads (blocking until available)
243
+ result = network.memory.get(:sentiment, wait: true)
244
+ result = network.memory.get(:sentiment, wait: 30) # timeout in seconds
245
+
246
+ # Multiple keys
247
+ results = network.memory.get(:sentiment, :entities, :keywords, wait: 60)
248
+
249
+ # Subscribe to changes
250
+ network.memory.subscribe(:status) do |change|
251
+ puts "#{change.key} changed by #{change.writer}: #{change.value}"
252
+ end
253
+ ```
254
+
137
255
  ## MCP (Model Context Protocol)
138
256
 
139
257
  **MCP** allows robots to connect to external tool servers:
@@ -141,6 +259,7 @@ robot.reset_memory
141
259
  ```ruby
142
260
  robot = RobotLab.build(
143
261
  name: "developer",
262
+ system_prompt: "You are a developer assistant.",
144
263
  mcp: [
145
264
  { name: "filesystem", transport: { type: "stdio", command: "mcp-server-filesystem" } },
146
265
  { name: "github", transport: { type: "stdio", command: "mcp-server-github" } }
@@ -148,6 +267,8 @@ robot = RobotLab.build(
148
267
  )
149
268
  ```
150
269
 
270
+ MCP configuration follows a hierarchical resolution: `runtime > robot > network > global config`. Values can be `:none`, `:inherit`, or explicit arrays.
271
+
151
272
  ## Execution Flow
152
273
 
153
274
  ```mermaid
@@ -160,11 +281,12 @@ sequenceDiagram
160
281
  participant LLM
161
282
  participant Tool
162
283
 
163
- User->>Network: run(message, context)
164
- Network->>Pipeline: call(initial_result)
284
+ User->>Network: run(message: "...", **context)
285
+ Network->>Pipeline: call_parallel(initial_result)
165
286
  Pipeline->>Task: call(result)
166
287
  Task->>Robot: call(enhanced_result)
167
- Robot->>LLM: inference(messages, tools)
288
+ Robot->>Robot: extract_run_context(result)
289
+ Robot->>LLM: ask(message)
168
290
 
169
291
  alt Tool Call
170
292
  LLM-->>Robot: tool_call
@@ -175,7 +297,7 @@ sequenceDiagram
175
297
 
176
298
  LLM-->>Robot: response
177
299
  Robot-->>Task: RobotResult
178
- Task-->>Pipeline: result.continue(value)
300
+ Task-->>Pipeline: result.continue(robot_result)
179
301
 
180
302
  alt Optional Task Activated
181
303
  Pipeline->>Task: call activated task
@@ -185,21 +307,26 @@ sequenceDiagram
185
307
  Network-->>User: SimpleFlow::Result
186
308
  ```
187
309
 
188
- ## Conditional Routing
310
+ ## Conditional Routing with ClassifierRobot
189
311
 
190
- Use custom Robot subclasses to implement intelligent routing:
312
+ Use a custom Robot subclass to implement intelligent routing. Override `call(result)` to inspect the LLM output and activate optional tasks:
191
313
 
192
314
  ```ruby
193
315
  class ClassifierRobot < RobotLab::Robot
194
316
  def call(result)
195
- robot_result = run(**extract_run_context(result))
317
+ # Extract context and message from the pipeline result
318
+ context = extract_run_context(result)
319
+ message = context.delete(:message)
320
+
321
+ # Run the robot to classify the input
322
+ robot_result = run(message, **context)
196
323
 
197
324
  new_result = result
198
325
  .with_context(@name.to_sym, robot_result)
199
326
  .continue(robot_result)
200
327
 
201
- # Activate appropriate specialist
202
- category = robot_result.last_text_content.to_s.downcase
328
+ # Activate the appropriate specialist based on classification
329
+ category = robot_result.last_text_content.to_s.strip.downcase
203
330
  case category
204
331
  when /billing/ then new_result.activate(:billing)
205
332
  when /technical/ then new_result.activate(:technical)
@@ -207,8 +334,143 @@ class ClassifierRobot < RobotLab::Robot
207
334
  end
208
335
  end
209
336
  end
337
+
338
+ # Build the classifier (uses a .md template with YAML front matter)
339
+ classifier = ClassifierRobot.new(
340
+ name: "classifier",
341
+ template: :classifier,
342
+ model: "claude-sonnet-4"
343
+ )
344
+
345
+ # Build specialist robots
346
+ billing_robot = RobotLab.build(name: "billing", template: :billing)
347
+ technical_robot = RobotLab.build(name: "technical", template: :technical)
348
+ general_robot = RobotLab.build(name: "general", template: :general)
349
+
350
+ # Create network with optional task routing
351
+ network = RobotLab.create_network(name: "support_network") do
352
+ task :classifier, classifier, depends_on: :none
353
+ task :billing, billing_robot, depends_on: :optional
354
+ task :technical, technical_robot, depends_on: :optional
355
+ task :general, general_robot, depends_on: :optional
356
+ end
357
+
358
+ result = network.run(message: "I was charged twice for my subscription.")
359
+ puts result.value.last_text_content
360
+ ```
361
+
362
+ ## Message Bus
363
+
364
+ 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.
365
+
366
+ ```ruby
367
+ bus = TypedBus::MessageBus.new
368
+
369
+ bob = RobotLab.build(name: "bob", system_prompt: "You tell jokes.", bus: bus)
370
+ alice = RobotLab.build(name: "alice", system_prompt: "You evaluate jokes.", bus: bus)
371
+
372
+ # Register handlers
373
+ bob.on_message do |message|
374
+ joke = bob.run(message.content.to_s).last_text_content
375
+ bob.send_reply(to: message.from.to_sym, content: joke, in_reply_to: message.key)
376
+ end
377
+
378
+ alice.on_message do |message|
379
+ verdict = alice.run("Is this funny? #{message.content}").last_text_content
380
+ # Send another request if not satisfied
381
+ alice.send_message(to: :bob, content: "Try again.") unless verdict.start_with?("FUNNY")
382
+ end
383
+
384
+ # Start the conversation
385
+ alice.send_message(to: :bob, content: "Tell me a robot joke.")
210
386
  ```
211
387
 
388
+ Key features:
389
+
390
+ - **Typed channels** — only `RobotMessage` objects accepted per channel
391
+ - **Auto-ACK** — 1-arg `on_message` blocks auto-acknowledge; 2-arg blocks give manual control
392
+ - **Reply correlation** — `send_reply(to:, content:, in_reply_to:)` tracks threads via `in_reply_to`
393
+ - **Independent of Network** — bus works without a Network pipeline
394
+
395
+ ### Dynamic Spawning
396
+
397
+ Robots can create new robots at runtime using `spawn`. The bus is created lazily:
398
+
399
+ ```ruby
400
+ dispatcher = RobotLab.build(name: "dispatcher", system_prompt: "You delegate work.")
401
+
402
+ # spawn creates a child on the same bus (bus created automatically)
403
+ helper = dispatcher.spawn(name: "helper", system_prompt: "You answer questions.")
404
+ answer = helper.run("What is 2+2?").last_text_content
405
+ helper.send_message(to: :dispatcher, content: answer)
406
+ ```
407
+
408
+ Robots can also join a bus after creation with `with_bus`:
409
+
410
+ ```ruby
411
+ bot = RobotLab.build(name: "latecomer", system_prompt: "Hello.")
412
+ bot.with_bus(existing_bus)
413
+ ```
414
+
415
+ Multiple robots with the same name enable fan-out — messages sent to that name are delivered to all subscribers.
416
+
417
+ ## Templates
418
+
419
+ 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).
420
+
421
+ ```markdown
422
+ ---
423
+ description: Customer support classifier
424
+ model: claude-sonnet-4
425
+ temperature: 0.3
426
+ ---
427
+ You are a request classifier. Analyze the user's request and classify it
428
+ as either "billing", "technical", or "general".
429
+
430
+ Respond with ONLY the category name, nothing else.
431
+ ```
432
+
433
+ Reference templates by symbol when building robots:
434
+
435
+ ```ruby
436
+ robot = RobotLab.build(
437
+ name: "classifier",
438
+ template: :classifier, # loads prompts/classifier.md
439
+ context: { tone: "professional" } # variables passed to the template
440
+ )
441
+ ```
442
+
443
+ ### Front Matter Keys
444
+
445
+ Templates support two categories of front matter keys:
446
+
447
+ **LLM Config:** `model`, `temperature`, `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop` — applied to the robot's chat configuration.
448
+
449
+ **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.
450
+
451
+ ```markdown
452
+ ---
453
+ description: GitHub assistant with MCP tool access
454
+ robot_name: github_bot
455
+ tools:
456
+ - CodeSearchTool
457
+ mcp:
458
+ - name: github
459
+ transport: stdio
460
+ command: npx
461
+ args: ["-y", "@modelcontextprotocol/server-github"]
462
+ model: claude-sonnet-4
463
+ ---
464
+ You are a GitHub assistant. Use available tools to help with repository tasks.
465
+ ```
466
+
467
+ ```ruby
468
+ # Template provides everything — minimal constructor
469
+ robot = RobotLab.build(template: :github_assistant)
470
+ ```
471
+
472
+ Constructor-provided values (`local_tools:`, `mcp:`, `name:`, `description:`) always take precedence over front matter values.
473
+
212
474
  ## Next Steps
213
475
 
214
476
  - [Quick Start Guide](getting-started/quick-start.md) - Build your first robot