robot_lab 0.2.8 → 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 (262) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -0
  3. data/README.md +16 -18
  4. data/lib/robot_lab/ask_user.rb +3 -3
  5. data/lib/robot_lab/config/defaults.yml +5 -5
  6. data/lib/robot_lab/config.rb +2 -2
  7. data/lib/robot_lab/mcp/transports/streamable_http.rb +2 -4
  8. data/lib/robot_lab/memory.rb +16 -7
  9. data/lib/robot_lab/robot/hooking.rb +26 -0
  10. data/lib/robot_lab/robot/result_building.rb +119 -0
  11. data/lib/robot_lab/robot.rb +37 -120
  12. data/lib/robot_lab/run_config.rb +52 -20
  13. data/lib/robot_lab/tool.rb +7 -12
  14. data/lib/robot_lab/version.rb +1 -1
  15. data/lib/robot_lab.rb +3 -3
  16. metadata +15 -274
  17. data/.architecture/AGENTS.md +0 -32
  18. data/.architecture/config.yml +0 -8
  19. data/.architecture/members.yml +0 -60
  20. data/.architecture/reviews/feature-free-will.md +0 -490
  21. data/.architecture/reviews/overall-codebase.md +0 -427
  22. data/.claude/settings.local.json +0 -57
  23. data/.codex/config.toml +0 -2
  24. data/.envrc +0 -6
  25. data/.github/workflows/deploy-github-pages.yml +0 -52
  26. data/.irbrc +0 -6
  27. data/.loki +0 -65
  28. data/.quality/reek_baseline.txt +0 -43
  29. data/.rubocop.yml +0 -10
  30. data/Archspec.rb +0 -44
  31. data/CLAUDE.md +0 -139
  32. data/COMMITS.md +0 -196
  33. data/Rakefile +0 -193
  34. data/_typos.toml +0 -21
  35. data/agent2agent_review.md +0 -192
  36. data/agentf_improvements.md +0 -253
  37. data/agents.md +0 -14
  38. data/docs/api/core/index.md +0 -147
  39. data/docs/api/core/memory.md +0 -578
  40. data/docs/api/core/network.md +0 -553
  41. data/docs/api/core/result.md +0 -211
  42. data/docs/api/core/robot.md +0 -1667
  43. data/docs/api/core/state.md +0 -145
  44. data/docs/api/core/tool.md +0 -520
  45. data/docs/api/errors.md +0 -323
  46. data/docs/api/hooks.md +0 -469
  47. data/docs/api/index.md +0 -137
  48. data/docs/api/mcp/client.md +0 -387
  49. data/docs/api/mcp/index.md +0 -267
  50. data/docs/api/mcp/server.md +0 -252
  51. data/docs/api/mcp/transports.md +0 -339
  52. data/docs/api/messages/index.md +0 -126
  53. data/docs/api/messages/text-message.md +0 -170
  54. data/docs/api/messages/tool-call-message.md +0 -206
  55. data/docs/api/messages/tool-result-message.md +0 -242
  56. data/docs/api/messages/user-message.md +0 -232
  57. data/docs/api/skills.md +0 -423
  58. data/docs/api/streaming/context.md +0 -280
  59. data/docs/api/streaming/events.md +0 -185
  60. data/docs/api/streaming/index.md +0 -156
  61. data/docs/api/support.md +0 -846
  62. data/docs/architecture/core-concepts.md +0 -575
  63. data/docs/architecture/index.md +0 -252
  64. data/docs/architecture/message-flow.md +0 -366
  65. data/docs/architecture/network-orchestration.md +0 -478
  66. data/docs/architecture/robot-execution.md +0 -396
  67. data/docs/architecture/state-management.md +0 -410
  68. data/docs/assets/css/custom.css +0 -56
  69. data/docs/assets/images/robot_lab.jpg +0 -0
  70. data/docs/concepts.md +0 -641
  71. data/docs/examples/basic-chat.md +0 -258
  72. data/docs/examples/index.md +0 -425
  73. data/docs/examples/mcp-server.md +0 -403
  74. data/docs/examples/multi-robot-network.md +0 -476
  75. data/docs/examples/tool-usage.md +0 -468
  76. data/docs/getting-started/configuration.md +0 -645
  77. data/docs/getting-started/index.md +0 -56
  78. data/docs/getting-started/installation.md +0 -200
  79. data/docs/getting-started/quick-start.md +0 -293
  80. data/docs/guides/building-robots.md +0 -1187
  81. data/docs/guides/creating-networks.md +0 -736
  82. data/docs/guides/hooks.md +0 -1206
  83. data/docs/guides/index.md +0 -78
  84. data/docs/guides/knowledge.md +0 -219
  85. data/docs/guides/mcp-integration.md +0 -740
  86. data/docs/guides/memory.md +0 -458
  87. data/docs/guides/observability.md +0 -733
  88. data/docs/guides/streaming.md +0 -323
  89. data/docs/guides/using-tools.md +0 -780
  90. data/docs/index.md +0 -280
  91. data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
  92. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
  93. data/examples/.envrc +0 -1
  94. data/examples/01_simple_robot.rb +0 -31
  95. data/examples/02_tools.rb +0 -109
  96. data/examples/03_network.rb +0 -118
  97. data/examples/04_mcp.rb +0 -215
  98. data/examples/05_streaming.rb +0 -137
  99. data/examples/06_prompt_templates.rb +0 -325
  100. data/examples/07_network_memory.rb +0 -313
  101. data/examples/08_llm_config.rb +0 -162
  102. data/examples/09_chaining.rb +0 -251
  103. data/examples/10_memory.rb +0 -315
  104. data/examples/11_network_introspection.rb +0 -239
  105. data/examples/12_message_bus.rb +0 -67
  106. data/examples/13_spawn.rb +0 -85
  107. data/examples/14_rusty_circuit/.envrc +0 -1
  108. data/examples/14_rusty_circuit/.gitignore +0 -1
  109. data/examples/14_rusty_circuit/README.md +0 -1
  110. data/examples/14_rusty_circuit/comic.rb +0 -149
  111. data/examples/14_rusty_circuit/display.rb +0 -217
  112. data/examples/14_rusty_circuit/heckler.rb +0 -65
  113. data/examples/14_rusty_circuit/open_mic.rb +0 -132
  114. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
  115. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
  116. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
  117. data/examples/14_rusty_circuit/scout.rb +0 -165
  118. data/examples/14_rusty_circuit/show.log +0 -234
  119. data/examples/15_memory_network_and_bus/.envrc +0 -1
  120. data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
  121. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
  122. data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
  123. data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
  124. data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
  125. data/examples/15_memory_network_and_bus/output/combined_article.md +0 -13
  126. data/examples/15_memory_network_and_bus/output/final_article.md +0 -13
  127. data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
  128. data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
  129. data/examples/15_memory_network_and_bus/output/memory.json +0 -13
  130. data/examples/15_memory_network_and_bus/output/revision_1.md +0 -18
  131. data/examples/15_memory_network_and_bus/output/revision_2.md +0 -13
  132. data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
  133. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
  134. data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
  135. data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
  136. data/examples/16_writers_room/.envrc +0 -1
  137. data/examples/16_writers_room/display.rb +0 -158
  138. data/examples/16_writers_room/output/.gitignore +0 -4
  139. data/examples/16_writers_room/output/README.md +0 -69
  140. data/examples/16_writers_room/output/opus_001.md +0 -263
  141. data/examples/16_writers_room/output/opus_001_notes.log +0 -470
  142. data/examples/16_writers_room/output/opus_002.md +0 -245
  143. data/examples/16_writers_room/output/opus_002_notes.log +0 -546
  144. data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
  145. data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
  146. data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
  147. data/examples/16_writers_room/prompts/writer.md +0 -37
  148. data/examples/16_writers_room/room.rb +0 -186
  149. data/examples/16_writers_room/tools.rb +0 -173
  150. data/examples/16_writers_room/writer.rb +0 -121
  151. data/examples/16_writers_room/writers_room.rb +0 -256
  152. data/examples/17_skills.rb +0 -234
  153. data/examples/18_rails/.envrc +0 -3
  154. data/examples/18_rails/.gitignore +0 -5
  155. data/examples/18_rails/Gemfile +0 -11
  156. data/examples/18_rails/README.md +0 -67
  157. data/examples/18_rails/Rakefile +0 -4
  158. data/examples/18_rails/app/controllers/application_controller.rb +0 -4
  159. data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
  160. data/examples/18_rails/app/jobs/application_job.rb +0 -4
  161. data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
  162. data/examples/18_rails/app/models/application_record.rb +0 -5
  163. data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
  164. data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
  165. data/examples/18_rails/app/robots/chat_robot.rb +0 -25
  166. data/examples/18_rails/app/tools/time_tool.rb +0 -9
  167. data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
  168. data/examples/18_rails/app/views/chat/index.html.erb +0 -67
  169. data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
  170. data/examples/18_rails/bin/dev +0 -7
  171. data/examples/18_rails/bin/rails +0 -6
  172. data/examples/18_rails/bin/setup +0 -15
  173. data/examples/18_rails/config/application.rb +0 -33
  174. data/examples/18_rails/config/cable.yml +0 -2
  175. data/examples/18_rails/config/database.yml +0 -5
  176. data/examples/18_rails/config/environment.rb +0 -4
  177. data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
  178. data/examples/18_rails/config/routes.rb +0 -6
  179. data/examples/18_rails/config.ru +0 -4
  180. data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
  181. data/examples/19_token_tracking.rb +0 -138
  182. data/examples/20_circuit_breaker.rb +0 -147
  183. data/examples/21_learning_loop.rb +0 -181
  184. data/examples/22_context_compression.rb +0 -172
  185. data/examples/23_convergence.rb +0 -133
  186. data/examples/24_structured_delegation.rb +0 -151
  187. data/examples/25_history_search/conversation.jsonl +0 -30
  188. data/examples/25_history_search.rb +0 -133
  189. data/examples/26_document_store/api_versioning_adr.md +0 -52
  190. data/examples/26_document_store/incident_postmortem.md +0 -46
  191. data/examples/26_document_store/postgres_runbook.md +0 -49
  192. data/examples/26_document_store/redis_caching_guide.md +0 -48
  193. data/examples/26_document_store/sidekiq_guide.md +0 -51
  194. data/examples/26_document_store.rb +0 -140
  195. data/examples/27_incident_response/incident_response.rb +0 -261
  196. data/examples/28_mcp_discovery.rb +0 -113
  197. data/examples/29_ractor_tools.rb +0 -240
  198. data/examples/30_ractor_network.rb +0 -252
  199. data/examples/31_launch_assessment.rb +0 -246
  200. data/examples/32_newsletter_reader.rb +0 -193
  201. data/examples/33_stock_generator.rb +0 -80
  202. data/examples/33_stock_predictor.rb +0 -327
  203. data/examples/34_agentskills.rb +0 -76
  204. data/examples/35_hooks.rb +0 -266
  205. data/examples/README.md +0 -533
  206. data/examples/common.rb +0 -148
  207. data/examples/prompts/assistant.md +0 -5
  208. data/examples/prompts/audit_trail.md +0 -8
  209. data/examples/prompts/billing.md +0 -10
  210. data/examples/prompts/classifier.md +0 -7
  211. data/examples/prompts/comedian.md +0 -6
  212. data/examples/prompts/comedy_critic.md +0 -10
  213. data/examples/prompts/configurable.md +0 -9
  214. data/examples/prompts/dispatcher.md +0 -12
  215. data/examples/prompts/entity_extractor.md +0 -14
  216. data/examples/prompts/escalation.md +0 -42
  217. data/examples/prompts/frontmatter_mcp_test.md +0 -9
  218. data/examples/prompts/frontmatter_named_test.md +0 -5
  219. data/examples/prompts/frontmatter_tools_test.md +0 -6
  220. data/examples/prompts/general.md +0 -7
  221. data/examples/prompts/github_assistant.md +0 -14
  222. data/examples/prompts/helper.md +0 -4
  223. data/examples/prompts/incident_responder.md +0 -18
  224. data/examples/prompts/keyword_extractor.md +0 -11
  225. data/examples/prompts/llm_config_demo.md +0 -20
  226. data/examples/prompts/order_support.md +0 -35
  227. data/examples/prompts/os_advocate.md +0 -13
  228. data/examples/prompts/os_chief.md +0 -13
  229. data/examples/prompts/os_editor.md +0 -13
  230. data/examples/prompts/parameterized_main_test.md +0 -6
  231. data/examples/prompts/pii_redactor.md +0 -7
  232. data/examples/prompts/product_support.md +0 -37
  233. data/examples/prompts/runbook_protocol.md +0 -12
  234. data/examples/prompts/sentiment_analyzer.md +0 -12
  235. data/examples/prompts/skill_a_test.md +0 -4
  236. data/examples/prompts/skill_b_test.md +0 -4
  237. data/examples/prompts/skill_config_test.md +0 -5
  238. data/examples/prompts/skill_cycle_a_test.md +0 -6
  239. data/examples/prompts/skill_cycle_b_test.md +0 -6
  240. data/examples/prompts/skill_description_test.md +0 -4
  241. data/examples/prompts/skill_leaf_test.md +0 -4
  242. data/examples/prompts/skill_nested_test.md +0 -6
  243. data/examples/prompts/skill_refs_main_test.md +0 -6
  244. data/examples/prompts/skill_self_ref_test.md +0 -6
  245. data/examples/prompts/skill_with_mcp_test.md +0 -9
  246. data/examples/prompts/skill_with_params_test.md +0 -6
  247. data/examples/prompts/skill_with_robot_name_test.md +0 -5
  248. data/examples/prompts/skill_with_tools_test.md +0 -6
  249. data/examples/prompts/sre_compliance.md +0 -10
  250. data/examples/prompts/structured_output.md +0 -7
  251. data/examples/prompts/synthesizer.md +0 -17
  252. data/examples/prompts/technical.md +0 -10
  253. data/examples/prompts/template_with_skills_test.md +0 -6
  254. data/examples/prompts/triage.md +0 -22
  255. data/examples/ruboruby.md +0 -423
  256. data/examples/xyzzy.rb +0 -97
  257. data/logfile +0 -8
  258. data/mkdocs.yml +0 -209
  259. data/robot_concurrency.md +0 -38
  260. data/simple_acp_review.md +0 -298
  261. data/temp.md +0 -6
  262. 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` | Semantic cache (RubyLLM::SemanticCache) |
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