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
data/docs/concepts.md DELETED
@@ -1,641 +0,0 @@
1
- # Core Concepts
2
-
3
- Understanding the fundamental concepts in RobotLab will help you build effective AI applications.
4
-
5
- ## Robot
6
-
7
- A **Robot** is an LLM-powered agent that inherits from `RubyLLM::Agent`. Each robot wraps a persistent chat session created at initialization and provides template-based prompts, tools, memory, and MCP integration. Robots are created using keyword arguments via the `RobotLab.build` factory method.
8
-
9
- Each robot has:
10
-
11
- - **Name**: An identifier. Nothing is auto-generated — if you omit `name:` the robot is literally named `"robot"`. The value is load-bearing: a robot still named `"robot"` is treated as "unnamed", which is what lets a template's `robot_name:` front matter key take effect. Give every robot an explicit name.
12
- - **Template**: A `.md` file with YAML front matter managed by prompt_manager, referenced by symbol
13
- - **System Prompt**: Inline instructions (can be used alone or combined with a template)
14
- - **Model**: The LLM model to use (defaults to `RobotLab.config.ruby_llm.model`)
15
- - **Provider**: Optional LLM provider for local models (Ollama, GPUStack, etc.)
16
- - **Skills**: Composable template behaviors prepended before the main template
17
- - **Local Tools**: `RubyLLM::Tool` subclasses or `RobotLab::Tool` instances (with automatic error handling)
18
- - **Streaming**: Real-time content via stored `on_content` callback or per-call block
19
- - **Memory**: Persistent key-value store across runs
20
-
21
- ```ruby
22
- # Robot with template (references prompts/support.md)
23
- robot = RobotLab.build(
24
- name: "support_agent",
25
- template: :support,
26
- context: { tone: "friendly", department: "billing" },
27
- local_tools: [OrderLookup, RefundProcessor],
28
- model: "claude-sonnet-4"
29
- )
30
-
31
- # Attached tools are only sent when the run asks for them
32
- robot.run("Where is order 4471?", tools: :inherit)
33
-
34
- # Robot with inline system prompt
35
- robot = RobotLab.build(
36
- name: "helper",
37
- system_prompt: "You are a friendly customer support agent."
38
- )
39
-
40
- # Bare robot configured via chaining
41
- robot = RobotLab.build(name: "bot")
42
- robot.with_instructions("Be concise.").with_temperature(0.3).run("Hello")
43
- ```
44
-
45
- The primary method is `robot.run("message")`, which takes a positional string argument and returns a `RobotResult`:
46
-
47
- ```ruby
48
- result = robot.run("What is 2 + 2?")
49
- puts result.last_text_content # => "4"
50
- ```
51
-
52
- Standalone robots persist their conversation history and memory across runs:
53
-
54
- ```ruby
55
- robot.run("My name is Alice.")
56
- result = robot.run("What is my name?")
57
- puts result.last_text_content # => "Your name is Alice."
58
- ```
59
-
60
- ## Configuration
61
-
62
- RobotLab uses `MywayConfig` for configuration. Values are loaded automatically from multiple sources in priority order (lowest to highest):
63
-
64
- 1. Bundled defaults (`lib/robot_lab/config/defaults.yml`)
65
- 2. Environment-specific overrides (development, test, production)
66
- 3. XDG user config (`~/.config/robot_lab/robot_lab.yml`)
67
- 4. Project config (`./config/robot_lab.yml`)
68
- 5. Environment variables (`ROBOT_LAB_*` prefix)
69
- 6. Constructor parameters
70
-
71
- ```ruby
72
- # Access configuration values
73
- RobotLab.config.ruby_llm.model #=> "claude-sonnet-4"
74
- RobotLab.config.ruby_llm.request_timeout #=> 120
75
-
76
- # Set API keys via environment variables
77
- # ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...
78
- # ROBOT_LAB_RUBY_LLM__OPENAI_API_KEY=sk-...
79
-
80
- # A configure block also exists, for runtime-only attributes such as the logger
81
- RobotLab.configure do |c|
82
- c.logger = Logger.new(File::NULL)
83
- end
84
-
85
- # Reload configuration
86
- RobotLab.reload_config!
87
- ```
88
-
89
- > [!IMPORTANT]
90
- > Two easy mistakes. First, the user config file is
91
- > `~/.config/robot_lab/**robot_lab**.yml` — the filename repeats the app name, and
92
- > `config.yml` is never read. Second, the `defaults:` wrapper used inside the gem's
93
- > bundled `defaults.yml` is silently ignored in your own files; write keys flat, or
94
- > under a section named for the current environment. The user file honours a
95
- > `development:` / `test:` / `production:` section; `./config/robot_lab.yml` must be
96
- > flat outside Rails but **must** be environment-sectioned under Rails. See
97
- > [Configuration](getting-started/configuration.md) for the full matrix.
98
-
99
- ## Network
100
-
101
- A **Network** is a collection of robots orchestrated using [SimpleFlow](https://github.com/MadBomber/simple_flow) pipelines. Networks provide:
102
-
103
- - **Task-Based Orchestration**: Define tasks with dependencies and routing
104
- - **Parallel Execution**: Tasks with the same dependencies run concurrently
105
- - **Optional Task Activation**: Dynamic routing based on robot output
106
- - **Per-Task Configuration**: Each task can have its own context, tools, and MCP servers
107
- - **Shared Memory**: All robots in a network share a reactive memory instance
108
-
109
- ```ruby
110
- network = RobotLab.create_network(name: "customer_service") do
111
- task :classifier, classifier_robot, depends_on: :none
112
- task :billing, billing_robot,
113
- context: { department: "billing" },
114
- depends_on: :optional
115
- task :technical, technical_robot,
116
- context: { department: "technical" },
117
- depends_on: :optional
118
- end
119
-
120
- result = network.run(message: "I was charged twice for my subscription.")
121
- ```
122
-
123
- ## Task
124
-
125
- A **Task** wraps a robot for use in a network pipeline with per-task configuration:
126
-
127
- - **Context**: Task-specific context deep-merged with network run params
128
- - **MCP**: MCP servers available to this task (`:none`, `:inherit`, or a name array)
129
- - **Tools**: Tools available to this task (`:none`, `:inherit`, or a name array)
130
- - **Memory**: Task-specific memory
131
- - **Dependencies**: `:none`, `[:task1, :task2]`, or `:optional`
132
- - **Config**: a `RunConfig` — but see the caveat below
133
- - **Poller group**: `poller_group:` (defaults to `:default`)
134
-
135
- ```ruby
136
- # The robot must already have the tools ATTACHED...
137
- billing_robot = RobotLab.build(
138
- name: "billing",
139
- system_prompt: "You handle billing.",
140
- local_tools: [RefundTool, InvoiceTool, AuditTool] # attached as CLASSES
141
- )
142
-
143
- task :billing, billing_robot,
144
- context: { department: "billing", escalation_level: 2 },
145
- tools: [RefundTool, InvoiceTool], # ...and this SELECTS from them
146
- depends_on: :optional
147
- ```
148
-
149
- > [!WARNING]
150
- > An explicit `tools:` array is a **name allowlist** over tools the robot already
151
- > has attached — it is not a way to attach new tools, and it is not a
152
- > local-vs-MCP switch. Attach tools with `local_tools:` when building the robot,
153
- > then filter here.
154
- >
155
- > **The allowlist entries must match the form the tool was attached in.** Matching
156
- > is a string comparison against each attached tool's `name`, and `Class#name`
157
- > differs from `RubyLLM::Tool#name`:
158
- >
159
- > | Attached as | Matching allowlist entry | Does not match |
160
- > |---|---|---|
161
- > | `local_tools: [RefundTool]` (class) | `[RefundTool]` or `%w[RefundTool]` | `%w[refund]` |
162
- > | `local_tools: [RefundTool.new]` (instance) | `%w[refund]` | `[RefundTool]` |
163
- >
164
- > A task-level `config:` is merged into the network config, so like a
165
- > network-level config it propagates **only `mcp` and `tools`** — not `model`,
166
- > `temperature`, or callbacks.
167
-
168
- ## SimpleFlow::Result
169
-
170
- Networks use `SimpleFlow::Result` for data flow between tasks:
171
-
172
- ```ruby
173
- result.value # Current task's output (RobotResult)
174
- result.context # Accumulated context, keyed by ROBOT name
175
- result.continue? # Whether the pipeline is still continuing
176
- result.errors # Accumulated errors
177
- ```
178
-
179
- > [!NOTE]
180
- > There is no `halted?` and no `continued?` — both raise `NoMethodError`. The
181
- > predicate is `continue?`. Note also that `result.context` is keyed by each
182
- > robot's `name:`, not by its task name.
183
-
184
- ### Result Methods
185
-
186
- The complete public API is `activate`, `activated_steps`, `context`, `continue`,
187
- `continue?`, `errors`, `halt`, `value`, `with_context`, and `with_error`.
188
-
189
- | Method | Purpose |
190
- |--------|---------|
191
- | `continue(value)` | Continue to next tasks |
192
- | `halt(value)` | Stop pipeline execution |
193
- | `with_context(key, val)` | Add data to context |
194
- | `with_error(key, message)` | Record an error |
195
- | `activate(task_name)` | Enable optional task |
196
- | `activated_steps` | Tasks activated so far |
197
-
198
- ## Tool
199
-
200
- **Tools** give robots the ability to interact with external systems. `RobotLab::Tool` extends `RubyLLM::Tool` with graceful error handling — if `execute` raises a `StandardError`, the error is caught and returned as a plain-text string (`"Error (tool_name): message"`) so the LLM can reason about it. Critical tools can opt out with `self.raise_on_error = true`.
201
-
202
- There are two patterns for defining tools:
203
-
204
- ### RubyLLM::Tool Subclass (Preferred)
205
-
206
- ```ruby
207
- class Calculator < RubyLLM::Tool
208
- description "Performs basic arithmetic operations"
209
-
210
- param :operation, type: "string", desc: "The operation (add, subtract, multiply, divide)"
211
- param :a, type: "number", desc: "First operand"
212
- param :b, type: "number", desc: "Second operand"
213
-
214
- def execute(operation:, a:, b:)
215
- case operation
216
- when "add" then a + b
217
- when "subtract" then a - b
218
- when "multiply" then a * b
219
- when "divide" then a.to_f / b
220
- else "Unknown operation: #{operation}"
221
- end
222
- end
223
- end
224
-
225
- robot = RobotLab.build(
226
- name: "math_bot",
227
- system_prompt: "You can do math.",
228
- local_tools: [Calculator]
229
- )
230
-
231
- # tools: :inherit is required -- run() sends no tools by default
232
- robot.run("What is 17 * 23?", tools: :inherit)
233
- ```
234
-
235
- > [!WARNING]
236
- > `Robot#run` defaults to `mcp: :none, tools: :none`, and an explicit `:none`
237
- > means "send zero tools this turn". Attaching tools with `local_tools:` does
238
- > **not** by itself make them available — a plain `robot.run("...")` sends the
239
- > model no tools at all. Pass `tools: :inherit` on the run (and
240
- > `mcp: :inherit, tools: :inherit` for MCP servers).
241
- >
242
- > For a **standalone** robot, do not pass `tools: :inherit` to `RobotLab.build`.
243
- > Build-time `:inherit` resolves against the parent level, which for a standalone
244
- > robot is the global `:none` — that produces an allowlist matching nothing and
245
- > suppresses the tools even when the run asks for them. Leave `tools:` unset.
246
- >
247
- > Inside a **network** it means the opposite: build-time `tools: :inherit` is
248
- > exactly how a robot opts into the allowlist carried by the network's `config:`.
249
- > With `RunConfig.new(tools: %w[RefundTool])` on the network and `tools: :inherit`
250
- > on both the robot and its task, the robot sends only `refund`; leaving the
251
- > robot's `tools:` unset sends everything it has attached instead.
252
-
253
- ### RobotLab::Tool.create Factory
254
-
255
- ```ruby
256
- tool = RobotLab::Tool.create(
257
- name: "get_weather",
258
- description: "Get current weather for a location",
259
- parameters: {
260
- type: "object",
261
- properties: {
262
- location: { type: "string", description: "City name" }
263
- },
264
- required: ["location"]
265
- }
266
- ) { |args| WeatherService.current(args[:location]) }
267
- ```
268
-
269
- ## RobotResult
270
-
271
- `RobotResult` captures the output of a single `robot.run(...)` call:
272
-
273
- ```ruby
274
- result = robot.run("Hello!")
275
-
276
- result.last_text_content # => "Hi there!" (String or nil)
277
- result.reply # => alias for last_text_content
278
- result.output # => [TextMessage] built from the final response text
279
- result.tool_calls # => [] (see note below -- effectively always empty)
280
- result.robot_name # => "assistant"
281
- result.stop_reason # => nil (always -- see note below)
282
- result.has_tool_calls? # => false
283
- result.checksum # => "a1b2c3d4..." (for dedup)
284
- result.duration # => Float or nil (elapsed seconds, set in pipeline execution)
285
- result.raw # => raw LLM response object
286
- ```
287
-
288
- > [!NOTE]
289
- > `result.tool_calls` and `result.has_tool_calls?` read the **final** assistant
290
- > message. By the time `run` returns, ruby_llm's tool loop has already completed
291
- > and that message carries no tool calls — so in practice `tool_calls` is always
292
- > empty and `has_tool_calls?` is always `false`. To observe tool activity, use
293
- > the `on_tool_call` / `on_tool_result` callbacks instead. Likewise
294
- > `result.output` is a single-element array built from the final response text,
295
- > not a transcript of the whole turn.
296
- >
297
- > `result.stop_reason` is likewise **always `nil`**: `RubyLLM::Message` does not
298
- > define `stop_reason`, and `RobotResult` only populates the field when the
299
- > response responds to it. It is dropped from `result.export` for the same reason.
300
- > Because `stopped?` is derived from the absence of tool calls, it is
301
- > correspondingly always `true`. Do not branch on `"end_turn"` / `"tool_use"`.
302
-
303
- ## Memory
304
-
305
- **Memory** is a reactive key-value store that provides persistent storage across robot executions. Standalone robots use their own inherent memory; robots in a network share the network's memory.
306
-
307
- ```ruby
308
- # Standalone robot with inherent memory
309
- robot = RobotLab.build(name: "assistant", system_prompt: "You are helpful.")
310
- robot.run("My name is Alice")
311
- robot.run("What's my name?") # Memory persists across runs
312
-
313
- # Access robot's memory directly
314
- robot.memory[:user_id] = 123
315
- robot.memory.data[:category] = "billing"
316
- robot.memory.data.category # => "billing" (method-style access)
317
-
318
- # Runtime memory injection
319
- robot.run("Help me", memory: { session_id: "abc123" })
320
-
321
- # Reset the key-value store (does NOT clear chat history)
322
- robot.reset_memory
323
-
324
- # Clear chat history (does NOT touch the key-value store)
325
- robot.clear_messages(keep_system: true)
326
- ```
327
-
328
- > [!NOTE]
329
- > `reset_memory` and `clear_messages` are independent. A robot's conversation
330
- > history and its key-value memory are two separate stores; clearing one leaves
331
- > the other intact.
332
-
333
- ### Reserved Memory Keys
334
-
335
- | Key | Purpose |
336
- |-----|---------|
337
- | `:data` | Runtime data (StateProxy for method-style access) |
338
- | `:results` | Accumulated robot results |
339
- | `:messages` | Conversation history |
340
- | `:session_id` | Session identifier for history persistence |
341
- | `:cache` | Semantic cache (RubyLLM::SemanticCache when the optional gem is installed, else nil) |
342
-
343
- ### Reactive Memory in Networks
344
-
345
- In a network, shared memory supports pub/sub semantics for inter-robot communication:
346
-
347
- ```ruby
348
- # Robot A writes to shared memory
349
- network.memory.set(:sentiment, { score: 0.8 })
350
-
351
- # Robot B reads (blocking until available)
352
- result = network.memory.get(:sentiment, wait: true)
353
- result = network.memory.get(:sentiment, wait: 30) # timeout in seconds
354
-
355
- # Multiple keys -- the timeout applies PER MISSING KEY, not to the call as a whole
356
- results = network.memory.get(:sentiment, :entities, :keywords, wait: 60)
357
-
358
- # Subscribe to changes
359
- network.memory.subscribe(:status) do |change|
360
- puts "#{change.key} changed by #{change.writer}: #{change.value}"
361
- end
362
- ```
363
-
364
- > [!WARNING]
365
- > A blocking `get` that times out **raises `RobotLab::AwaitTimeout`** — it does
366
- > not return `nil`. Wrap it in a `rescue` if a missing key is an acceptable
367
- > outcome. With several keys, the timeout is applied to each missing key in turn,
368
- > so `get(:a, :b, :c, wait: 60)` can block for up to 180 seconds.
369
-
370
- ## MCP (Model Context Protocol)
371
-
372
- **MCP** allows robots to connect to external tool servers:
373
-
374
- ```ruby
375
- robot = RobotLab.build(
376
- name: "developer",
377
- system_prompt: "You are a developer assistant.",
378
- mcp: [
379
- { name: "filesystem", transport: { type: "stdio", command: "mcp-server-filesystem" } },
380
- { name: "github", transport: { type: "stdio", command: "mcp-server-github" } }
381
- ]
382
- )
383
-
384
- # Connect the servers AND expose their tools for this run
385
- robot.run("List the Ruby files in ./lib", mcp: :inherit, tools: :inherit)
386
- ```
387
-
388
- > [!IMPORTANT]
389
- > `transport:` must be a **nested hash** — `transport: { type: "stdio", command: ..., args: [...] }`.
390
- > A flat `transport: stdio` with sibling `command:`/`args:` keys raises
391
- > `NoMethodError: undefined method 'transform_keys' for an instance of String`,
392
- > and that exception is **swallowed** rather than raised: the robot builds
393
- > successfully with zero tools. It is not silent, though — a line is logged at
394
- > `WARN` through `RobotLab.config.logger` (`Robot 'dev' error connecting to MCP
395
- > server 'fs': undefined method 'transform_keys' for an instance of String`) and
396
- > the server name lands in `robot.failed_mcp_server_names`. An *invalid* transport
397
- > type is swallowed the same way; only a direct `MCP::Server.new` raises
398
- > `ArgumentError`. Valid transport types are `stdio`, `sse`, `ws`,
399
- > `websocket`, `streamable-http`, and `http` (`streamable_http` with an
400
- > underscore is invalid).
401
- >
402
- > `mcp:` also defaults to `:none` on `run`. `mcp: :inherit` triggers the
403
- > connection attempt; `tools: :inherit` is additionally required for the MCP
404
- > tools to reach the model. MCP connection failures are logged and recorded in
405
- > `robot.failed_mcp_server_names` — they are not raised.
406
-
407
- MCP configuration follows a hierarchical resolution: `runtime > robot > network > global config`. Values can be `:none`, `:inherit`, or explicit arrays.
408
-
409
- ## Execution Flow
410
-
411
- ```mermaid
412
- sequenceDiagram
413
- participant User
414
- participant Network
415
- participant Pipeline
416
- participant Task
417
- participant Robot
418
- participant LLM
419
- participant Tool
420
-
421
- User->>Network: run(message: "...", **context)
422
- Network->>Pipeline: call_parallel(initial_result)
423
- Pipeline->>Task: call(result)
424
- Task->>Robot: call(enhanced_result)
425
- Robot->>Robot: extract_run_context(result)
426
- Robot->>LLM: ask(message)
427
-
428
- alt Tool Call
429
- LLM-->>Robot: tool_call
430
- Robot->>Tool: execute(params)
431
- Tool-->>Robot: result
432
- Robot->>LLM: continue with result
433
- end
434
-
435
- LLM-->>Robot: response
436
- Robot-->>Task: RobotResult
437
- Task-->>Pipeline: result.continue(robot_result)
438
-
439
- alt Optional Task Activated
440
- Pipeline->>Task: call activated task
441
- end
442
-
443
- Pipeline-->>Network: final result
444
- Network-->>User: SimpleFlow::Result
445
- ```
446
-
447
- ## Conditional Routing with ClassifierRobot
448
-
449
- Use a custom Robot subclass to implement intelligent routing. Override `call(result)` to inspect the LLM output and activate optional tasks:
450
-
451
- ```ruby
452
- class ClassifierRobot < RobotLab::Robot
453
- def call(result)
454
- # Extract context and message from the pipeline result
455
- context = extract_run_context(result)
456
- message = context.delete(:message)
457
-
458
- # Run the robot to classify the input
459
- robot_result = run(message, **context)
460
-
461
- new_result = result
462
- .with_context(@name.to_sym, robot_result)
463
- .continue(robot_result)
464
-
465
- # Activate the appropriate specialist based on classification
466
- category = robot_result.last_text_content.to_s.strip.downcase
467
- case category
468
- when /billing/ then new_result.activate(:billing)
469
- when /technical/ then new_result.activate(:technical)
470
- else new_result.activate(:general)
471
- end
472
- end
473
- end
474
-
475
- # Build the classifier (uses a .md template with YAML front matter)
476
- classifier = ClassifierRobot.new(
477
- name: "classifier",
478
- template: :classifier,
479
- model: "claude-sonnet-4"
480
- )
481
-
482
- # Build specialist robots
483
- billing_robot = RobotLab.build(name: "billing", template: :billing)
484
- technical_robot = RobotLab.build(name: "technical", template: :technical)
485
- general_robot = RobotLab.build(name: "general", template: :general)
486
-
487
- # Create network with optional task routing
488
- network = RobotLab.create_network(name: "support_network") do
489
- task :classifier, classifier, depends_on: :none
490
- task :billing, billing_robot, depends_on: :optional
491
- task :technical, technical_robot, depends_on: :optional
492
- task :general, general_robot, depends_on: :optional
493
- end
494
-
495
- result = network.run(message: "I was charged twice for my subscription.")
496
- puts result.value.last_text_content
497
- ```
498
-
499
- ## Message Bus
500
-
501
- The **Message Bus** enables bidirectional, cyclic communication between robots via `typed_bus`. While Networks enforce DAG-based execution, the bus supports negotiation loops, convergence patterns, and multi-turn dialogues.
502
-
503
- ```ruby
504
- bus = TypedBus::MessageBus.new
505
-
506
- bob = RobotLab.build(name: "bob", system_prompt: "You tell jokes.", bus: bus)
507
- alice = RobotLab.build(name: "alice", system_prompt: "You evaluate jokes.", bus: bus)
508
-
509
- # Register handlers
510
- bob.on_message do |message|
511
- joke = bob.run(message.content.to_s).last_text_content
512
- bob.send_reply(to: message.from.to_sym, content: joke, in_reply_to: message.key)
513
- end
514
-
515
- alice.on_message do |message|
516
- verdict = alice.run("Is this funny? #{message.content}").last_text_content
517
- # Send another request if not satisfied
518
- alice.send_message(to: :bob, content: "Try again.") unless verdict.start_with?("FUNNY")
519
- end
520
-
521
- # Start the conversation
522
- alice.send_message(to: :bob, content: "Tell me a robot joke.")
523
- ```
524
-
525
- Key features:
526
-
527
- - **Typed channels** — only `RobotMessage` objects accepted per channel
528
- - **Auto-ACK** — 1-arg `on_message` blocks auto-acknowledge; 2-arg blocks give manual control
529
- - **Reply correlation** — `send_reply(to:, content:, in_reply_to:)` tracks threads via `in_reply_to`
530
- - **Independent of Network** — bus works without a Network pipeline
531
-
532
- ### Dynamic Spawning
533
-
534
- Robots can create new robots at runtime using `spawn`. The bus is created lazily:
535
-
536
- ```ruby
537
- dispatcher = RobotLab.build(name: "dispatcher", system_prompt: "You delegate work.")
538
-
539
- # spawn creates a child on the same bus (bus created automatically)
540
- helper = dispatcher.spawn(name: "helper", system_prompt: "You answer questions.")
541
- answer = helper.run("What is 2+2?").last_text_content
542
- helper.send_message(to: :dispatcher, content: answer)
543
- ```
544
-
545
- Robots can also join a bus after creation with `with_bus`:
546
-
547
- ```ruby
548
- bot = RobotLab.build(name: "latecomer", system_prompt: "Hello.")
549
- bot.with_bus(existing_bus)
550
- ```
551
-
552
- Multiple robots with the same name enable fan-out — messages sent to that name are delivered to all subscribers.
553
-
554
- ## Templates
555
-
556
- Templates are `.md` files with YAML front matter, managed by the prompt_manager gem. They live in the configured template path (default: `./prompts/` or `app/prompts/` in Rails).
557
-
558
- ```markdown
559
- ---
560
- description: Customer support classifier
561
- model: claude-sonnet-4
562
- temperature: 0.3
563
- ---
564
- You are a request classifier. Analyze the user's request and classify it
565
- as either "billing", "technical", or "general".
566
-
567
- Respond with ONLY the category name, nothing else.
568
- ```
569
-
570
- Reference templates by symbol when building robots:
571
-
572
- ```ruby
573
- robot = RobotLab.build(
574
- name: "classifier",
575
- template: :classifier, # loads prompts/classifier.md
576
- context: { tone: "professional" } # variables passed to the template
577
- )
578
- ```
579
-
580
- ### Front Matter Keys
581
-
582
- Templates support two categories of front matter keys:
583
-
584
- **LLM Config:** only `model` and `temperature` take effect.
585
-
586
- **Robot Extras:** `robot_name`, `description`, `tools`, `mcp`, `skills`, `parameters` — applied to the robot's identity and capabilities. These make templates self-contained: reading the `.md` file tells you everything about the robot.
587
-
588
- > [!WARNING]
589
- > Front matter is parsed into a `RunConfig` and then applied by dispatching
590
- > `chat.with_<field>` for each field the chat responds to. The chat object only
591
- > has `with_model` and `with_temperature`, so **`top_p`, `top_k`, `max_tokens`,
592
- > `presence_penalty`, `frequency_penalty`, and `stop` are parsed and silently
593
- > dropped** when declared in front matter. A template declaring all eight yields
594
- > a chat with empty params.
595
- >
596
- > Those six *do* work as constructor keyword arguments or via a `config:`
597
- > `RunConfig`, because that path goes through `with_params`:
598
- >
599
- > ```ruby
600
- > RobotLab.build(name: "bot", template: :writer, max_tokens: 2000, top_p: 0.3)
601
- > ```
602
-
603
- ```markdown
604
- ---
605
- description: GitHub assistant with MCP tool access
606
- robot_name: github_bot
607
- tools:
608
- - CodeSearchTool
609
- mcp:
610
- - name: github
611
- transport:
612
- type: stdio
613
- command: npx
614
- args: ["-y", "@modelcontextprotocol/server-github"]
615
- model: claude-sonnet-4
616
- ---
617
- You are a GitHub assistant. Use available tools to help with repository tasks.
618
- ```
619
-
620
- > [!IMPORTANT]
621
- > Note the **nested** `transport:` mapping above. A flat
622
- > `transport: stdio` with sibling `command:`/`args:` keys raises a `NoMethodError`
623
- > that is swallowed rather than propagated, leaving you with a robot that has no
624
- > tools. Look for the `WARN` line on `RobotLab.config.logger` and check
625
- > `robot.failed_mcp_server_names` — both record the failure.
626
-
627
- ```ruby
628
- # Template provides everything — minimal constructor
629
- robot = RobotLab.build(template: :github_assistant)
630
-
631
- # ...but the tools and MCP servers it declares still have to be requested per run
632
- robot.run("Open issues in MadBomber/robot_lab?", mcp: :inherit, tools: :inherit)
633
- ```
634
-
635
- Constructor-provided values (`local_tools:`, `mcp:`, `name:`, `description:`) always take precedence over front matter values. In the cascade, front matter is the **base**: template front matter → `config:` RunConfig → constructor keyword arguments, with constructor arguments always winning.
636
-
637
- ## Next Steps
638
-
639
- - [Quick Start Guide](getting-started/quick-start.md) - Build your first robot
640
- - [Building Robots](guides/building-robots.md) - Detailed robot creation guide
641
- - [Creating Networks](guides/creating-networks.md) - Network orchestration patterns