robot_lab 0.3.0 → 0.5.0

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