robot_lab 0.3.0 → 0.3.1

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 (256) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +1 -1
  4. data/lib/robot_lab/mcp/transports/streamable_http.rb +1 -1
  5. data/lib/robot_lab/version.rb +1 -1
  6. metadata +3 -253
  7. data/.architecture/AGENTS.md +0 -32
  8. data/.architecture/config.yml +0 -8
  9. data/.architecture/members.yml +0 -60
  10. data/.architecture/reviews/feature-free-will.md +0 -490
  11. data/.architecture/reviews/overall-codebase.md +0 -427
  12. data/.claude/settings.local.json +0 -57
  13. data/.codex/config.toml +0 -2
  14. data/.envrc +0 -6
  15. data/.github/workflows/deploy-github-pages.yml +0 -52
  16. data/.irbrc +0 -6
  17. data/.loki +0 -65
  18. data/.quality/reek_baseline.txt +0 -43
  19. data/.rubocop.yml +0 -10
  20. data/Archspec.rb +0 -44
  21. data/CLAUDE.md +0 -141
  22. data/COMMITS.md +0 -196
  23. data/Rakefile +0 -193
  24. data/_typos.toml +0 -21
  25. data/agent2agent_review.md +0 -192
  26. data/agentf_improvements.md +0 -253
  27. data/agents.md +0 -14
  28. data/docs/api/core/index.md +0 -147
  29. data/docs/api/core/memory.md +0 -578
  30. data/docs/api/core/network.md +0 -553
  31. data/docs/api/core/result.md +0 -211
  32. data/docs/api/core/robot.md +0 -1667
  33. data/docs/api/core/state.md +0 -145
  34. data/docs/api/core/tool.md +0 -520
  35. data/docs/api/errors.md +0 -323
  36. data/docs/api/hooks.md +0 -469
  37. data/docs/api/index.md +0 -137
  38. data/docs/api/mcp/client.md +0 -387
  39. data/docs/api/mcp/index.md +0 -267
  40. data/docs/api/mcp/server.md +0 -252
  41. data/docs/api/mcp/transports.md +0 -339
  42. data/docs/api/messages/index.md +0 -126
  43. data/docs/api/messages/text-message.md +0 -170
  44. data/docs/api/messages/tool-call-message.md +0 -206
  45. data/docs/api/messages/tool-result-message.md +0 -242
  46. data/docs/api/messages/user-message.md +0 -232
  47. data/docs/api/skills.md +0 -423
  48. data/docs/api/streaming/context.md +0 -280
  49. data/docs/api/streaming/events.md +0 -185
  50. data/docs/api/streaming/index.md +0 -156
  51. data/docs/api/support.md +0 -846
  52. data/docs/architecture/core-concepts.md +0 -575
  53. data/docs/architecture/index.md +0 -252
  54. data/docs/architecture/message-flow.md +0 -366
  55. data/docs/architecture/network-orchestration.md +0 -478
  56. data/docs/architecture/robot-execution.md +0 -396
  57. data/docs/architecture/state-management.md +0 -410
  58. data/docs/assets/css/custom.css +0 -56
  59. data/docs/assets/images/robot_lab.jpg +0 -0
  60. data/docs/concepts.md +0 -641
  61. data/docs/examples/basic-chat.md +0 -258
  62. data/docs/examples/index.md +0 -425
  63. data/docs/examples/mcp-server.md +0 -403
  64. data/docs/examples/multi-robot-network.md +0 -476
  65. data/docs/examples/tool-usage.md +0 -468
  66. data/docs/getting-started/configuration.md +0 -645
  67. data/docs/getting-started/index.md +0 -56
  68. data/docs/getting-started/installation.md +0 -197
  69. data/docs/getting-started/quick-start.md +0 -293
  70. data/docs/guides/building-robots.md +0 -1187
  71. data/docs/guides/creating-networks.md +0 -736
  72. data/docs/guides/hooks.md +0 -1206
  73. data/docs/guides/index.md +0 -78
  74. data/docs/guides/knowledge.md +0 -219
  75. data/docs/guides/mcp-integration.md +0 -740
  76. data/docs/guides/memory.md +0 -458
  77. data/docs/guides/observability.md +0 -733
  78. data/docs/guides/streaming.md +0 -323
  79. data/docs/guides/using-tools.md +0 -780
  80. data/docs/index.md +0 -280
  81. data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
  82. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
  83. data/examples/.envrc +0 -3
  84. data/examples/01_simple_robot.rb +0 -31
  85. data/examples/02_tools.rb +0 -109
  86. data/examples/03_network.rb +0 -118
  87. data/examples/04_mcp.rb +0 -217
  88. data/examples/05_streaming.rb +0 -137
  89. data/examples/06_prompt_templates.rb +0 -325
  90. data/examples/07_network_memory.rb +0 -313
  91. data/examples/08_llm_config.rb +0 -162
  92. data/examples/09_chaining.rb +0 -251
  93. data/examples/10_memory.rb +0 -315
  94. data/examples/11_network_introspection.rb +0 -239
  95. data/examples/12_message_bus.rb +0 -67
  96. data/examples/13_spawn.rb +0 -85
  97. data/examples/14_rusty_circuit/.envrc +0 -1
  98. data/examples/14_rusty_circuit/.gitignore +0 -1
  99. data/examples/14_rusty_circuit/README.md +0 -1
  100. data/examples/14_rusty_circuit/comic.rb +0 -149
  101. data/examples/14_rusty_circuit/display.rb +0 -217
  102. data/examples/14_rusty_circuit/heckler.rb +0 -65
  103. data/examples/14_rusty_circuit/open_mic.rb +0 -132
  104. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
  105. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
  106. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
  107. data/examples/14_rusty_circuit/scout.rb +0 -165
  108. data/examples/14_rusty_circuit/show.log +0 -234
  109. data/examples/15_memory_network_and_bus/.envrc +0 -1
  110. data/examples/15_memory_network_and_bus/README.md +0 -66
  111. data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
  112. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
  113. data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
  114. data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
  115. data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
  116. data/examples/15_memory_network_and_bus/output/combined_article.md +0 -11
  117. data/examples/15_memory_network_and_bus/output/final_article.md +0 -8
  118. data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
  119. data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
  120. data/examples/15_memory_network_and_bus/output/memory.json +0 -13
  121. data/examples/15_memory_network_and_bus/output/revision_1.md +0 -29
  122. data/examples/15_memory_network_and_bus/output/revision_2.md +0 -50
  123. data/examples/15_memory_network_and_bus/output/revision_3.md +0 -8
  124. data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
  125. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
  126. data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
  127. data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
  128. data/examples/16_writers_room/.envrc +0 -1
  129. data/examples/16_writers_room/display.rb +0 -158
  130. data/examples/16_writers_room/output/.gitignore +0 -4
  131. data/examples/16_writers_room/output/README.md +0 -69
  132. data/examples/16_writers_room/output/opus_001.md +0 -263
  133. data/examples/16_writers_room/output/opus_001_notes.log +0 -470
  134. data/examples/16_writers_room/output/opus_002.md +0 -245
  135. data/examples/16_writers_room/output/opus_002_notes.log +0 -546
  136. data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
  137. data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
  138. data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
  139. data/examples/16_writers_room/prompts/writer.md +0 -37
  140. data/examples/16_writers_room/room.rb +0 -186
  141. data/examples/16_writers_room/tools.rb +0 -173
  142. data/examples/16_writers_room/writer.rb +0 -121
  143. data/examples/16_writers_room/writers_room.rb +0 -256
  144. data/examples/17_skills.rb +0 -234
  145. data/examples/18_rails/.envrc +0 -3
  146. data/examples/18_rails/.gitignore +0 -5
  147. data/examples/18_rails/Gemfile +0 -11
  148. data/examples/18_rails/README.md +0 -67
  149. data/examples/18_rails/Rakefile +0 -4
  150. data/examples/18_rails/app/controllers/application_controller.rb +0 -4
  151. data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
  152. data/examples/18_rails/app/jobs/application_job.rb +0 -4
  153. data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
  154. data/examples/18_rails/app/models/application_record.rb +0 -5
  155. data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
  156. data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
  157. data/examples/18_rails/app/robots/chat_robot.rb +0 -25
  158. data/examples/18_rails/app/tools/time_tool.rb +0 -9
  159. data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
  160. data/examples/18_rails/app/views/chat/index.html.erb +0 -67
  161. data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
  162. data/examples/18_rails/bin/dev +0 -7
  163. data/examples/18_rails/bin/rails +0 -6
  164. data/examples/18_rails/bin/setup +0 -15
  165. data/examples/18_rails/config/application.rb +0 -33
  166. data/examples/18_rails/config/cable.yml +0 -2
  167. data/examples/18_rails/config/database.yml +0 -5
  168. data/examples/18_rails/config/environment.rb +0 -4
  169. data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
  170. data/examples/18_rails/config/routes.rb +0 -6
  171. data/examples/18_rails/config.ru +0 -4
  172. data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
  173. data/examples/19_token_tracking.rb +0 -138
  174. data/examples/20_circuit_breaker.rb +0 -147
  175. data/examples/21_learning_loop.rb +0 -181
  176. data/examples/22_context_compression.rb +0 -172
  177. data/examples/23_convergence.rb +0 -133
  178. data/examples/24_structured_delegation.rb +0 -151
  179. data/examples/25_history_search/conversation.jsonl +0 -30
  180. data/examples/25_history_search.rb +0 -133
  181. data/examples/26_document_store/api_versioning_adr.md +0 -52
  182. data/examples/26_document_store/incident_postmortem.md +0 -46
  183. data/examples/26_document_store/postgres_runbook.md +0 -49
  184. data/examples/26_document_store/redis_caching_guide.md +0 -48
  185. data/examples/26_document_store/sidekiq_guide.md +0 -51
  186. data/examples/26_document_store.rb +0 -140
  187. data/examples/27_incident_response/README.md +0 -65
  188. data/examples/27_incident_response/incident_response.rb +0 -261
  189. data/examples/28_mcp_discovery.rb +0 -113
  190. data/examples/29_ractor_tools.rb +0 -240
  191. data/examples/30_ractor_network.rb +0 -252
  192. data/examples/31_launch_assessment.rb +0 -246
  193. data/examples/32_newsletter_reader.rb +0 -193
  194. data/examples/33_stock_generator.rb +0 -80
  195. data/examples/33_stock_predictor.rb +0 -327
  196. data/examples/34_agentskills.rb +0 -76
  197. data/examples/35_hooks.rb +0 -266
  198. data/examples/README.md +0 -550
  199. data/examples/common.rb +0 -180
  200. data/examples/prompts/assistant.md +0 -5
  201. data/examples/prompts/audit_trail.md +0 -8
  202. data/examples/prompts/billing.md +0 -10
  203. data/examples/prompts/classifier.md +0 -7
  204. data/examples/prompts/comedian.md +0 -6
  205. data/examples/prompts/comedy_critic.md +0 -10
  206. data/examples/prompts/configurable.md +0 -9
  207. data/examples/prompts/dispatcher.md +0 -12
  208. data/examples/prompts/entity_extractor.md +0 -14
  209. data/examples/prompts/escalation.md +0 -42
  210. data/examples/prompts/frontmatter_mcp_test.md +0 -9
  211. data/examples/prompts/frontmatter_named_test.md +0 -5
  212. data/examples/prompts/frontmatter_tools_test.md +0 -6
  213. data/examples/prompts/general.md +0 -7
  214. data/examples/prompts/github_assistant.md +0 -14
  215. data/examples/prompts/helper.md +0 -4
  216. data/examples/prompts/incident_responder.md +0 -18
  217. data/examples/prompts/keyword_extractor.md +0 -11
  218. data/examples/prompts/llm_config_demo.md +0 -20
  219. data/examples/prompts/order_support.md +0 -35
  220. data/examples/prompts/os_advocate.md +0 -13
  221. data/examples/prompts/os_chief.md +0 -13
  222. data/examples/prompts/os_editor.md +0 -13
  223. data/examples/prompts/parameterized_main_test.md +0 -6
  224. data/examples/prompts/pii_redactor.md +0 -7
  225. data/examples/prompts/product_support.md +0 -37
  226. data/examples/prompts/runbook_protocol.md +0 -12
  227. data/examples/prompts/sentiment_analyzer.md +0 -12
  228. data/examples/prompts/skill_a_test.md +0 -4
  229. data/examples/prompts/skill_b_test.md +0 -4
  230. data/examples/prompts/skill_config_test.md +0 -5
  231. data/examples/prompts/skill_cycle_a_test.md +0 -6
  232. data/examples/prompts/skill_cycle_b_test.md +0 -6
  233. data/examples/prompts/skill_description_test.md +0 -4
  234. data/examples/prompts/skill_leaf_test.md +0 -4
  235. data/examples/prompts/skill_nested_test.md +0 -6
  236. data/examples/prompts/skill_refs_main_test.md +0 -6
  237. data/examples/prompts/skill_self_ref_test.md +0 -6
  238. data/examples/prompts/skill_with_mcp_test.md +0 -9
  239. data/examples/prompts/skill_with_params_test.md +0 -6
  240. data/examples/prompts/skill_with_robot_name_test.md +0 -5
  241. data/examples/prompts/skill_with_tools_test.md +0 -6
  242. data/examples/prompts/sre_compliance.md +0 -10
  243. data/examples/prompts/structured_output.md +0 -7
  244. data/examples/prompts/synthesizer.md +0 -17
  245. data/examples/prompts/technical.md +0 -10
  246. data/examples/prompts/template_with_skills_test.md +0 -6
  247. data/examples/prompts/triage.md +0 -22
  248. data/examples/ruboruby.md +0 -423
  249. data/examples/run_all.rb +0 -60
  250. data/examples/xyzzy.rb +0 -97
  251. data/logfile +0 -8
  252. data/mkdocs.yml +0 -209
  253. data/robot_concurrency.md +0 -38
  254. data/simple_acp_review.md +0 -298
  255. data/temp.md +0 -6
  256. data/tool_manifest_plan.md +0 -155
@@ -1,575 +0,0 @@
1
- # Core Concepts
2
-
3
- This page provides an in-depth look at RobotLab's fundamental building blocks.
4
-
5
- ## Robot
6
-
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
-
9
- - A unique identity (name, description)
10
- - A personality (system prompt and/or template)
11
- - Capabilities (tools, MCP connections)
12
- - Model, provider, and inference configuration
13
- - Inherent memory (key-value store)
14
-
15
- ### Robot Anatomy
16
-
17
- ```ruby
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
23
- You are a friendly customer support agent for Acme Corp.
24
- Always be polite and helpful. If you don't know something,
25
- say so honestly.
26
- PROMPT
27
- local_tools: [OrderLookup, RefundProcessor], # RubyLLM::Tool subclasses
28
- mcp: :inherit, # Use the network's MCP servers
29
- temperature: 0.7 # Inference parameter
30
- )
31
- ```
32
-
33
- The build-time `mcp: :inherit` above assumes this robot will be added to a
34
- network whose `config:` supplies an `mcp:` list. The parent is resolved on every
35
- run, so `:inherit` picks up whatever the enclosing network provides. For a robot
36
- that will run **standalone**, `:inherit` resolves against the global default
37
- `:none` and yields nothing — give it an explicit array instead.
38
-
39
- Or with a template:
40
-
41
- ```ruby
42
- robot = RobotLab.build(
43
- name: "support_agent",
44
- template: :support, # Loads prompts/support.md
45
- context: { company: "Acme Corp" }, # Template variables
46
- local_tools: [OrderLookup]
47
- )
48
- ```
49
-
50
- ### Robot Lifecycle
51
-
52
- ```mermaid
53
- stateDiagram-v2
54
- [*] --> Created: RobotLab.build / Robot.new
55
- Created --> Running: robot.run("message")
56
- Running --> ToolLoop: tool_call from LLM
57
- ToolLoop --> Running: tool result sent back
58
- Running --> Completed: final text response
59
- Completed --> Running: robot.run("next message")
60
- Completed --> [*]: robot.disconnect
61
- ```
62
-
63
- The persistent `@chat` maintains conversation history across multiple `run` calls, making the robot stateful.
64
-
65
- ### Robot Properties
66
-
67
- | Property | Type | Description |
68
- |----------|------|-------------|
69
- | `name` | `String` | Unique identifier within network |
70
- | `description` | `String`, `nil` | What the robot does |
71
- | `model` | `String` | LLM model ID (resolved from chat) |
72
- | `template` | `Symbol`, `nil` | Prompt template identifier |
73
- | `system_prompt` | `String`, `nil` | Inline system prompt |
74
- | `local_tools` | `Array` | Locally defined tools |
75
- | `mcp_clients` | `Hash` | Connected MCP clients by server name |
76
- | `mcp_tools` | `Array` | Tools discovered from MCP servers |
77
- | `memory` | `Memory` | Inherent key-value memory |
78
- | `bus` | `TypedBus::MessageBus`, `nil` | Message bus instance |
79
- | `outbox` | `Hash` | Sent messages tracked with status and replies |
80
- | `mcp_config` | `Symbol`, `Array` | Build-time MCP configuration |
81
- | `tools_config` | `Symbol`, `Array` | Build-time tools configuration |
82
-
83
- ### Running a Robot
84
-
85
- The primary method is `robot.run("message")`:
86
-
87
- ```ruby
88
- result = robot.run("What is the weather in Berlin?")
89
- puts result.last_text_content
90
- ```
91
-
92
- `run` defaults to `mcp: :none, tools: :none`. Those defaults are *explicit* "send nothing this turn" values, not "unset" — so a plain `run` connects no MCP servers and sends the LLM zero tools even when `local_tools:`/`mcp:` were supplied at build time. Opt in per run:
93
-
94
- ```ruby
95
- robot.run("...", tools: :inherit) # send the attached local tools
96
- robot.run("...", mcp: :inherit, tools: :inherit) # connect MCP servers and send their tools
97
- ```
98
-
99
- With other runtime overrides:
100
-
101
- ```ruby
102
- result = robot.run("Analyze this",
103
- memory: { data: report },
104
- tools: :inherit
105
- )
106
- ```
107
-
108
- With streaming — the block receives a `RubyLLM::Chunk`, whose text is in `content`:
109
-
110
- ```ruby
111
- robot.run("Tell me a story") do |chunk|
112
- print chunk.content
113
- end
114
- ```
115
-
116
- An `on_content:` callback (constructor kwarg or `RunConfig` field) fires on every run. When both an `on_content` callback and a block are supplied, both fire, stored callback first.
117
-
118
- ## Tool
119
-
120
- Tools give robots the ability to interact with external systems. There are two patterns for defining tools.
121
-
122
- ### RubyLLM::Tool Subclass (Primary)
123
-
124
- ```ruby
125
- class GetWeather < RubyLLM::Tool
126
- description "Get current weather for a location"
127
-
128
- param :location, type: "string", desc: "City name"
129
- param :unit, type: "string", desc: "celsius or fahrenheit"
130
-
131
- def execute(location:, unit: "celsius")
132
- WeatherAPI.current(location, unit: unit)
133
- end
134
- end
135
- ```
136
-
137
- Tools defined as `RubyLLM::Tool` subclasses are passed to robots via `local_tools:`:
138
-
139
- ```ruby
140
- robot = RobotLab.build(
141
- name: "weather_bot",
142
- system_prompt: "You provide weather information.",
143
- local_tools: [GetWeather]
144
- )
145
- ```
146
-
147
- ### RobotLab::Tool.create Factory
148
-
149
- For simpler tools that do not need a class:
150
-
151
- ```ruby
152
- tool = RobotLab::Tool.create(
153
- name: "get_time",
154
- description: "Get the current time"
155
- ) { |_args| Time.now.to_s }
156
- ```
157
-
158
- With parameter schema:
159
-
160
- ```ruby
161
- tool = RobotLab::Tool.create(
162
- name: "get_weather",
163
- description: "Get weather for a location",
164
- parameters: {
165
- type: "object",
166
- properties: {
167
- location: { type: "string", description: "City name" }
168
- },
169
- required: ["location"]
170
- }
171
- ) { |args| WeatherAPI.current(args[:location]) }
172
- ```
173
-
174
- ### Tool Execution
175
-
176
- When an LLM decides to use a tool:
177
-
178
- 1. LLM generates a tool call with tool name and arguments
179
- 2. `@chat` (RubyLLM) identifies the tool from its registered tools
180
- 3. Calls the `execute` method with keyword arguments
181
- 4. Result is sent back to the LLM for continued processing
182
- 5. Loop repeats until the LLM produces a final text response
183
-
184
- ### Error Handling
185
-
186
- Tool errors are captured and returned to the LLM:
187
-
188
- ```ruby
189
- def execute(order_id:)
190
- order = ORDERS[order_id]
191
- if order
192
- order
193
- else
194
- { error: "Order not found" }
195
- end
196
- end
197
- ```
198
-
199
- ## Memory
200
-
201
- Memory is a reactive key-value store used by robots and networks.
202
-
203
- ### Standalone vs Network Memory
204
-
205
- - **Standalone**: Each robot has its own inherent `Memory` instance (`robot.memory`)
206
- - **In a Network**: All robots share the network's `Memory` instance
207
-
208
- ```ruby
209
- # Standalone memory
210
- robot.memory[:user_id] = 123
211
- robot.memory[:user_id] # => 123
212
-
213
- # Network memory is passed automatically
214
- network = RobotLab.create_network(name: "pipeline") do
215
- task :robot_a, robot_a, depends_on: :none
216
- task :robot_b, robot_b, depends_on: [:robot_a]
217
- end
218
- # Both robot_a and robot_b share network.memory during execution
219
- ```
220
-
221
- ### Reserved Keys
222
-
223
- | Key | Type | Description |
224
- |-----|------|-------------|
225
- | `:data` | `StateProxy` | Runtime data (hash-style `memory.data[:key]` and method-style `memory.data.key_name`) |
226
- | `:results` | `Array` | Accumulated robot results |
227
- | `:messages` | `Array` | Conversation history |
228
- | `:session_id` | `String` | Session identifier |
229
- | `:cache` | `Module`, `nil` | Semantic cache (RubyLLM::SemanticCache when the optional gem is installed) |
230
-
231
- ### Reactive Features
232
-
233
- Memory supports pub/sub semantics for inter-robot communication:
234
-
235
- ```ruby
236
- # Write a value (notifies subscribers, wakes waiters)
237
- memory.set(:sentiment, { score: 0.8 })
238
-
239
- # Read a value (non-blocking)
240
- memory.get(:sentiment) # => { score: 0.8 } or nil
241
-
242
- # Blocking read (waits until value exists)
243
- memory.get(:sentiment, wait: true) # Blocks indefinitely
244
- memory.get(:sentiment, wait: 30) # Blocks up to 30s, then raises RobotLab::AwaitTimeout
245
-
246
- # Subscribe to changes
247
- memory.subscribe(:sentiment) do |change|
248
- puts "#{change.key} = #{change.value} (written by #{change.writer})"
249
- end
250
- ```
251
-
252
- ## Message Types
253
-
254
- RobotLab uses a type hierarchy for messages:
255
-
256
- ```mermaid
257
- classDiagram
258
- Message <|-- TextMessage
259
- Message <|-- ToolCallMessage
260
- Message <|-- ToolResultMessage
261
-
262
- class Message {
263
- +String type
264
- +String role
265
- +content
266
- +String stop_reason
267
- +text?()
268
- +tool_call?()
269
- +tool_result?()
270
- +stopped?()
271
- }
272
-
273
- class TextMessage {
274
- +String content
275
- }
276
-
277
- class ToolCallMessage {
278
- +Array~ToolMessage~ tools
279
- }
280
-
281
- class ToolResultMessage {
282
- +ToolMessage tool
283
- +Hash content
284
- +success?()
285
- +error?()
286
- }
287
- ```
288
-
289
- ### Message Roles
290
-
291
- | Role | Description |
292
- |------|-------------|
293
- | `user` | Input from the user |
294
- | `assistant` | Response from the LLM |
295
- | `system` | System instructions |
296
- | `tool_result` | Tool execution result |
297
-
298
- ### Stop Reasons
299
-
300
- | Reason | Description |
301
- |--------|-------------|
302
- | `stop` | Natural completion |
303
- | `tool` | Tool call requested |
304
-
305
- ## RobotResult
306
-
307
- The output from a robot execution:
308
-
309
- ```ruby
310
- result = robot.run("Hello!")
311
-
312
- result.robot_name # => "support_agent"
313
- result.output # => [TextMessage] built from the final response text
314
- result.tool_calls # => [] — see note below
315
- result.stop_reason # => nil — always (see below)
316
- result.created_at # => Time
317
- result.id # => UUID string
318
- result.input_tokens # => Integer
319
- result.output_tokens # => Integer
320
- result.duration # => Float, nil (set by Robot#call during pipeline execution)
321
- result.checksum # => "sha256-hex"
322
- ```
323
-
324
- `stop_reason` is always `nil` on a `Robot#run` result — `build_result` only copies it when the response responds to `stop_reason`, and `RubyLLM::Message` does not. The table above describes `Message::VALID_STOP_REASONS`, which applies to message objects you construct yourself, not to `RobotResult`. As a consequence `result.stopped?` reduces to `!result.has_tool_calls?`.
325
-
326
- `output` is always a single `TextMessage` synthesized from the final response text — it is not a transcript of the turn. `tool_calls` is read off that final assistant message, which no longer carries tool calls once RubyLLM's tool loop has finished, so in practice it is empty; use the `on_tool_call`/`on_tool_result` callbacks or the tool hooks to observe tool usage.
327
-
328
- ### Accessing Response Content
329
-
330
- ```ruby
331
- # Get last text response (most common)
332
- text = result.last_text_content
333
-
334
- # Check if tools were called
335
- has_tools = result.has_tool_calls?
336
-
337
- # Check if execution completed naturally
338
- result.stopped?
339
-
340
- # Serialization
341
- result.export # => Hash (excludes debug fields)
342
- result.to_h # => Hash (includes debug fields)
343
- result.to_json # => JSON string
344
- ```
345
-
346
- ## Configuration
347
-
348
- Global configuration uses `MywayConfig`. Sources are layered lowest to highest:
349
-
350
- 1. Bundled defaults (`lib/robot_lab/config/defaults.yml`)
351
- 2. Environment-specific overrides (development / test / production)
352
- 3. XDG config file (`~/.config/robot_lab/robot_lab.yml` — note the filename repeats the app name; `config.yml` is never read, and this loader does **not** run ERB)
353
- 4. Project config (`./config/robot_lab.yml` — ERB is evaluated here)
354
- 5. Environment variables (`ROBOT_LAB_*` prefix, double underscore for nesting)
355
- 6. Constructor params
356
-
357
- The two file layers treat top-level wrappers differently:
358
-
359
- - **XDG file** (`~/.config/robot_lab/robot_lab.yml`) — a section named for the current environment **is** honoured. The loader checks `parsed.key?(env)` first (env comes from `Anyway::Settings.current_environment`, then `RAILS_ENV`, then `RACK_ENV`, defaulting to `"development"`) and falls back to the root when no such key exists. So `development:` works, a flat file works, and `production:` is simply skipped while you are in development. Only a `defaults:` wrapper is meaningless — it is not an environment name, so the whole hash is read as flat config and `defaults` is an unknown key.
360
- - **Project file** (`./config/robot_lab.yml`) — outside Rails this must be flat; every wrapper, `defaults:` and environment names alike, is ignored. Inside Rails, `anyway_config` sets `current_environment` to `Rails.env`, which makes this file environmental: a flat project file is then ignored and keys must be nested under `development:` / `test:` / `production:`.
361
-
362
- Read via `RobotLab.config`, or set values with the `RobotLab.configure` block, which yields the same `Config` object:
363
-
364
- ```ruby
365
- RobotLab.config.ruby_llm.model # => "claude-sonnet-4"
366
- RobotLab.config.ruby_llm.request_timeout # => 120
367
-
368
- RobotLab.configure do |c|
369
- c.logger = Logger.new($stdout)
370
- end
371
- ```
372
-
373
- ### RunConfig
374
-
375
- Global `Config` is distinct from `RunConfig`, the per-run settings object that flows `RobotLab.config → Network → Task → Robot → template front matter → constructor kwargs`. Its fields are:
376
-
377
- - **LLM**: `model`, `temperature`, `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop`
378
- - **Capabilities** (`TOOL_FIELDS`): `mcp`, `tools`
379
- - **Callbacks**: `on_tool_call`, `on_tool_result`, `on_content`
380
- - **Infrastructure** (`INFRA_FIELDS`): `bus`, `enable_cache`, `max_tool_rounds`, `token_budget`, `cost_budget`, `ractor_pool_size`, `max_concurrent_robots`, `doom_loop_threshold`, `auto_compact`, `compact_threshold`, `max_tools`
381
-
382
- `TOOL_FIELDS` is exactly `[:mcp, :tools]` — those are the two a network propagates. `max_tools` is an infrastructure field, so it is *not* inherited that way.
383
-
384
- Per robot, template front matter is the **base**, a `config:` RunConfig merges over it, and constructor kwargs always win. A network-level `config:` propagates only `mcp` and `tools` down to member robots (and only when a robot opts in with `:inherit`); LLM fields and callbacks are never inherited from a network. `max_concurrent_robots` is the one field the network itself consumes.
385
-
386
- ## Configuration Hierarchy
387
-
388
- Tools and MCP servers use a cascading configuration system:
389
-
390
- ```
391
- RobotLab.config (global)
392
- |
393
- +-- mcp: [server1, server2]
394
- +-- tools: [tool1, tool2]
395
- |
396
- +-- Network config: (RunConfig)
397
- | |
398
- | +-- mcp: :inherit | :none | [servers]
399
- | +-- tools: :inherit | :none | [tools]
400
- | |
401
- | +-- Robot (build-time config)
402
- | |
403
- | +-- mcp: :inherit | :none | [servers]
404
- | +-- tools: :inherit | :none | [tools]
405
- | |
406
- | +-- Task (per-task) / run() call -- the RUNTIME level
407
- | +-- mcp: :none (default) | :inherit | [servers]
408
- | +-- tools: :none (default) | :inherit | [tools]
409
- ```
410
-
411
- Resolution order: **task/runtime > robot build-time > network > global config**.
412
-
413
- A `Task`'s `mcp:`/`tools:` are not a separate tier between network and robot. The task injects them into `run_params`, and `Robot#call` pulls them out and passes them to `run` — so they arrive as the *runtime* value and are resolved against the robot's build-time value.
414
-
415
- The `:inherit` value pulls from the parent level. `:none` explicitly disables. An explicit array is a **filter over the already-attached tools**, not a local-vs-MCP switch.
416
-
417
- Three consequences worth internalizing:
418
-
419
- - `run()` defaults both to `:none`, so the runtime level is a deliberate "send nothing this turn" unless you override it. Pass `tools: :inherit` (and `mcp: :inherit`) to use what the robot was built with.
420
- - Build-time `:inherit` depends on there being a parent to inherit from. The parent is recomputed on every run (`network_config&.tools || network_parent_config(network)&.tools || RobotLab.config.tools`), so inside a network whose `config:` sets `tools:`/`mcp:` it is exactly the right way to opt in. For a robot that runs **standalone** the parent is the global `:none`, so build-time `:inherit` matches nothing — leave `tools:` unset there and opt in at run time.
421
- - An explicit array is compared with `tool.name.to_s`, and `Class#name` differs from `RubyLLM::Tool#name`. A tool attached as a class matches `[RefundTool]` (`"RefundTool"`); the same tool attached as an instance matches `%w[refund]`. Both forms work — but the array must be written in the same form the tool was attached in, or it filters everything out. Note that the **build-time** `tools:` kwarg is validated (`validate_tools_filter!`) and accepts only Strings/Symbols; the class form is available only at the task/runtime level, which is not validated. See [Network Orchestration](network-orchestration.md#task-configuration).
422
-
423
- ## Message Bus
424
-
425
- 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.
426
-
427
- ### How It Works
428
-
429
- 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`. Delivery is routed through a shared `BusPoller`, which runs the handler **in the caller's execution context** (Async fiber or OS thread) rather than on a background thread of its own. A mutex serializes deliveries per robot: if a robot is already processing a message, later ones are queued and drained after the current one returns.
430
-
431
- ```ruby
432
- bus = TypedBus::MessageBus.new
433
-
434
- bob = RobotLab.build(name: "bob", system_prompt: "You tell jokes.", bus: bus)
435
- alice = RobotLab.build(name: "alice", system_prompt: "You evaluate jokes.", bus: bus)
436
- ```
437
-
438
- ### RobotMessage
439
-
440
- `RobotMessage` is an immutable `Data.define` value object used as the typed envelope:
441
-
442
- | Field | Type | Description |
443
- |-------|------|-------------|
444
- | `id` | `Integer` | Per-robot sequential counter |
445
- | `from` | `String` | Sender's robot name (= channel name) |
446
- | `content` | `String`, `Hash` | Message payload |
447
- | `in_reply_to` | `String`, `nil` | Composite key of the original message (e.g., `"alice:1"`) |
448
-
449
- Methods: `key` returns `"from:id"` composite identity; `reply?` returns true when `in_reply_to` is set.
450
-
451
- ### Sending and Receiving
452
-
453
- ```ruby
454
- # Send a message to another robot
455
- alice.send_message(to: :bob, content: "Tell me a joke.")
456
-
457
- # Handle incoming messages with auto-ack (1 arg)
458
- bob.on_message do |message|
459
- joke = bob.run(message.content.to_s).last_text_content
460
- bob.send_reply(to: message.from.to_sym, content: joke, in_reply_to: message.key)
461
- end
462
- ```
463
-
464
- Block arity controls delivery handling: 1 argument auto-acks; 2 arguments give manual control over `delivery.ack!`/`delivery.nack!`.
465
-
466
- `send_message`/`send_reply` synchronize the per-robot message counter and outbox with an internal mutex, so concurrent sends from multiple threads and reply correlation can't clobber each other — the bus is safe to send on from more than one thread at a time. (There is no poller thread doing the correlating: as described above, `BusPoller#enqueue` processes deliveries inline in the caller's context.)
467
-
468
- For the common case of a robot that should simply answer whatever tasks arrive on the bus, `respond_to_tasks`/`serve` do the `on_message` wiring above in one call:
469
-
470
- ```ruby
471
- bob.serve # equivalent to the on_message block above, running bob.run and replying automatically
472
- ```
473
-
474
- `respond_to_tasks` takes a block instead when the reply needs post-processing, and both ignore inbound messages that are themselves replies, so two robots calling `serve` on each other don't loop. See [Auto-Responding to Bus Tasks](../guides/building-robots.md#auto-responding-to-bus-tasks).
475
-
476
- ### Dynamic Spawning
477
-
478
- Robots can create new robots at runtime using `spawn`. The bus is created lazily — no upfront wiring required:
479
-
480
- ```ruby
481
- dispatcher = RobotLab.build(name: "dispatcher", system_prompt: "You delegate work.")
482
-
483
- # spawn creates a child on the same bus (bus created automatically)
484
- helper = dispatcher.spawn(name: "helper", system_prompt: "You answer questions.")
485
-
486
- # The child can immediately communicate with the parent
487
- answer = helper.run("What is 2+2?").last_text_content
488
- helper.send_message(to: :dispatcher, content: answer)
489
- ```
490
-
491
- The spawned robot inherits its parent's `model`/`provider`, so a dispatcher running on a local provider (e.g. Ollama) spawns specialists targeting that same model instead of falling back to the global default (which would fail without cloud credentials). Explicit `model:`/`provider:` passed to `spawn` still override.
492
-
493
- Robots can also join a bus after creation using `with_bus`:
494
-
495
- ```ruby
496
- bot = RobotLab.build(name: "late_joiner", system_prompt: "Hello.")
497
- bot.with_bus(existing_bus) # now connected to the bus
498
- ```
499
-
500
- **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:
501
-
502
- ```ruby
503
- worker1 = dispatcher.spawn(name: "worker", system_prompt: "Worker 1")
504
- worker2 = dispatcher.spawn(name: "worker", system_prompt: "Worker 2")
505
- dispatcher.send_message(to: :worker, content: "Do this task")
506
- # Both worker1 and worker2 receive the message
507
- ```
508
-
509
- ### Bus vs Network
510
-
511
- | Feature | Network | Message Bus |
512
- |---------|---------|-------------|
513
- | Execution model | DAG (acyclic) | Cyclic, bidirectional |
514
- | Communication | Sequential pipeline | Pub/sub channels |
515
- | Memory | Shared network memory | Independent per-robot |
516
- | Use case | Linear workflows | Negotiation, convergence |
517
-
518
- The bus is purely additive — robots without `bus:` work exactly as before.
519
-
520
- ## Network
521
-
522
- A Network orchestrates multiple robots in a pipeline workflow using SimpleFlow:
523
-
524
- ```ruby
525
- network = RobotLab.create_network(name: "support") do
526
- task :classifier, classifier_robot, depends_on: :none
527
- task :billing, billing_robot, depends_on: :optional
528
- task :technical, technical_robot, depends_on: :optional
529
- end
530
-
531
- result = network.run(message: "I need help with billing")
532
- ```
533
-
534
- Networks provide:
535
-
536
- - **DAG-based execution** via SimpleFlow with `depends_on:` for sequencing
537
- - **Parallel execution** for tasks with the same dependencies
538
- - **Optional tasks** activated dynamically by classifier robots
539
- - **Shared memory** for inter-robot communication
540
- - **Per-task configuration** via the `Task` wrapper
541
- - **Broadcast messaging** for network-wide announcements
542
-
543
- Robots can be added to a network without a pipeline task via `add_robot`, and removed again with `remove_robot(name)` (returns the removed robot, or `nil` if absent). `remove_robot` only drops the robot from the crew — it doesn't rewrite the pipeline, so avoid removing a robot that's still referenced by a task's `depends_on`.
544
-
545
- ## Runnable Protocol
546
-
547
- `RobotLab::Runnable` is a shared interface implemented by both `Robot` and `Network`, so callers can treat either uniformly instead of branching on `is_a?(RobotLab::Network)`:
548
-
549
- ```ruby
550
- def summarize(runnable)
551
- runnable.crew.each { |r| puts r.name }
552
- puts "chief: #{runnable.chief.name}"
553
- puts runnable.network? ? "network of #{runnable.robot_count}" : "single robot"
554
- end
555
-
556
- summarize(robot) # crew: [robot], chief: robot, "single robot"
557
- summarize(network) # crew: network.robots.values, chief: crew.first, "network of N"
558
- ```
559
-
560
- | Method | Robot | Network |
561
- |--------|-------|---------|
562
- | `crew` | `[self]` | `robots.values` (pipeline order) |
563
- | `chief` | `self` | `crew.first` |
564
- | `robot_count` | `1` | `crew.size` |
565
- | `network?` | `false` | `true` |
566
- | `single?` | `true` | `false` |
567
- | `run(message = nil, **opts)` | accepts a positional message (already did) | now also accepts a positional message, folded into `message:` — the keyword form still works |
568
-
569
- `crew` is the only method implementers must define themselves; `chief`, `robot_count`, and `single?` all derive from it, and `network?` defaults to `false` unless overridden (as `Network` does).
570
-
571
- ## Next Steps
572
-
573
- - [Robot Execution](robot-execution.md) - Detailed execution flow
574
- - [Network Orchestration](network-orchestration.md) - Multi-robot coordination
575
- - [Using Tools](../guides/using-tools.md) - Creating and using tools