robot_lab 0.1.0 → 0.2.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 (242) hide show
  1. checksums.yaml +4 -4
  2. data/.architecture/AGENTS.md +32 -0
  3. data/.architecture/config.yml +8 -0
  4. data/.architecture/members.yml +60 -0
  5. data/.architecture/reviews/feature-free-will.md +490 -0
  6. data/.architecture/reviews/overall-codebase.md +427 -0
  7. data/.claude/settings.local.json +57 -0
  8. data/.codex/config.toml +2 -0
  9. data/.irbrc +2 -2
  10. data/.rubocop.yml +172 -0
  11. data/CHANGELOG.md +72 -0
  12. data/CLAUDE.md +139 -0
  13. data/README.md +91 -95
  14. data/Rakefile +109 -3
  15. data/agent2agent_review.md +192 -0
  16. data/agentf_improvements.md +253 -0
  17. data/agents.md +14 -0
  18. data/docs/examples/index.md +37 -2
  19. data/docs/getting-started/configuration.md +20 -7
  20. data/docs/guides/index.md +16 -16
  21. data/docs/guides/knowledge.md +7 -1
  22. data/docs/guides/observability.md +132 -0
  23. data/docs/index.md +30 -3
  24. data/docs/superpowers/plans/2026-05-06-agentskills.md +1303 -0
  25. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +247 -0
  26. data/examples/.envrc +1 -0
  27. data/examples/01_simple_robot.rb +5 -9
  28. data/examples/02_tools.rb +5 -9
  29. data/examples/03_network.rb +8 -9
  30. data/examples/04_mcp.rb +21 -29
  31. data/examples/05_streaming.rb +12 -18
  32. data/examples/06_prompt_templates.rb +11 -19
  33. data/examples/07_network_memory.rb +16 -31
  34. data/examples/08_llm_config.rb +10 -22
  35. data/examples/09_chaining.rb +16 -27
  36. data/examples/10_memory.rb +12 -28
  37. data/examples/11_network_introspection.rb +15 -29
  38. data/examples/12_message_bus.rb +5 -12
  39. data/examples/13_spawn.rb +5 -10
  40. data/examples/14_rusty_circuit/.envrc +1 -0
  41. data/examples/14_rusty_circuit/comic.rb +2 -0
  42. data/examples/14_rusty_circuit/heckler.rb +1 -1
  43. data/examples/14_rusty_circuit/open_mic.rb +1 -3
  44. data/examples/14_rusty_circuit/scout.rb +2 -0
  45. data/examples/15_memory_network_and_bus/.envrc +1 -0
  46. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +6 -3
  47. data/examples/15_memory_network_and_bus/linux_writer.rb +1 -1
  48. data/examples/15_memory_network_and_bus/output/combined_article.md +6 -6
  49. data/examples/15_memory_network_and_bus/output/final_article.md +6 -8
  50. data/examples/15_memory_network_and_bus/output/linux_draft.md +4 -2
  51. data/examples/15_memory_network_and_bus/output/mac_draft.md +3 -3
  52. data/examples/15_memory_network_and_bus/output/memory.json +6 -6
  53. data/examples/15_memory_network_and_bus/output/revision_1.md +10 -11
  54. data/examples/15_memory_network_and_bus/output/revision_2.md +6 -8
  55. data/examples/15_memory_network_and_bus/output/windows_draft.md +3 -3
  56. data/examples/16_writers_room/.envrc +1 -0
  57. data/examples/16_writers_room/writers_room.rb +2 -4
  58. data/examples/17_skills.rb +8 -17
  59. data/examples/18_rails/Gemfile +1 -0
  60. data/examples/19_token_tracking.rb +9 -15
  61. data/examples/20_circuit_breaker.rb +10 -19
  62. data/examples/21_learning_loop.rb +11 -20
  63. data/examples/22_context_compression.rb +6 -13
  64. data/examples/23_convergence.rb +6 -17
  65. data/examples/24_structured_delegation.rb +11 -15
  66. data/examples/25_history_search.rb +5 -12
  67. data/examples/26_document_store.rb +6 -13
  68. data/examples/27_incident_response/incident_response.rb +4 -5
  69. data/examples/28_mcp_discovery.rb +8 -11
  70. data/examples/29_ractor_tools.rb +4 -9
  71. data/examples/30_ractor_network.rb +10 -19
  72. data/examples/31_launch_assessment.rb +10 -23
  73. data/examples/32_newsletter_reader.rb +188 -0
  74. data/examples/33_stock_generator.rb +80 -0
  75. data/examples/33_stock_predictor.rb +306 -0
  76. data/examples/34_agentskills.rb +72 -0
  77. data/examples/README.md +1 -1
  78. data/examples/common.rb +76 -0
  79. data/examples/ruboruby.md +423 -0
  80. data/examples/temp.md +51 -0
  81. data/lib/robot_lab/agent_skill.rb +63 -0
  82. data/lib/robot_lab/agent_skill_catalog.rb +74 -0
  83. data/lib/robot_lab/ask_user.rb +2 -2
  84. data/lib/robot_lab/bus_poller.rb +12 -5
  85. data/lib/robot_lab/config.rb +1 -12
  86. data/lib/robot_lab/delegation_future.rb +1 -1
  87. data/lib/robot_lab/doom_loop_detector.rb +98 -0
  88. data/lib/robot_lab/history_compressor.rb +4 -10
  89. data/lib/robot_lab/mcp/client.rb +1 -2
  90. data/lib/robot_lab/mcp/connection_poller.rb +3 -3
  91. data/lib/robot_lab/mcp/server.rb +1 -1
  92. data/lib/robot_lab/mcp/server_discovery.rb +0 -2
  93. data/lib/robot_lab/memory.rb +32 -27
  94. data/lib/robot_lab/memory_change.rb +2 -2
  95. data/lib/robot_lab/message.rb +4 -4
  96. data/lib/robot_lab/network.rb +11 -6
  97. data/lib/robot_lab/robot/agent_skill_matching.rb +99 -0
  98. data/lib/robot_lab/robot/bus_messaging.rb +9 -27
  99. data/lib/robot_lab/robot/history_search.rb +4 -1
  100. data/lib/robot_lab/robot/mcp_management.rb +5 -11
  101. data/lib/robot_lab/robot/template_rendering.rb +60 -40
  102. data/lib/robot_lab/robot.rb +323 -206
  103. data/lib/robot_lab/robot_result.rb +6 -5
  104. data/lib/robot_lab/run_config.rb +5 -11
  105. data/lib/robot_lab/script_tool.rb +76 -0
  106. data/lib/robot_lab/state_proxy.rb +7 -5
  107. data/lib/robot_lab/tool.rb +3 -3
  108. data/lib/robot_lab/tool_config.rb +1 -1
  109. data/lib/robot_lab/tool_manifest.rb +5 -7
  110. data/lib/robot_lab/user_message.rb +2 -2
  111. data/lib/robot_lab/version.rb +1 -1
  112. data/lib/robot_lab/waiter.rb +1 -1
  113. data/lib/robot_lab.rb +41 -52
  114. data/logfile +8 -0
  115. data/mkdocs.yml +2 -3
  116. data/robot_concurrency.md +38 -0
  117. data/simple_acp_review.md +298 -0
  118. data/site/404.html +2300 -0
  119. data/site/api/core/index.html +2706 -0
  120. data/site/api/core/memory/index.html +3793 -0
  121. data/site/api/core/network/index.html +3500 -0
  122. data/site/api/core/robot/index.html +4566 -0
  123. data/site/api/core/state/index.html +3390 -0
  124. data/site/api/core/tool/index.html +3843 -0
  125. data/site/api/index.html +2635 -0
  126. data/site/api/mcp/client/index.html +3435 -0
  127. data/site/api/mcp/index.html +2783 -0
  128. data/site/api/mcp/server/index.html +3252 -0
  129. data/site/api/mcp/transports/index.html +3352 -0
  130. data/site/api/messages/index.html +2641 -0
  131. data/site/api/messages/text-message/index.html +3087 -0
  132. data/site/api/messages/tool-call-message/index.html +3159 -0
  133. data/site/api/messages/tool-result-message/index.html +3252 -0
  134. data/site/api/messages/user-message/index.html +3212 -0
  135. data/site/api/streaming/context/index.html +3282 -0
  136. data/site/api/streaming/events/index.html +3347 -0
  137. data/site/api/streaming/index.html +2738 -0
  138. data/site/architecture/core-concepts/index.html +3757 -0
  139. data/site/architecture/index.html +2797 -0
  140. data/site/architecture/message-flow/index.html +3238 -0
  141. data/site/architecture/network-orchestration/index.html +3433 -0
  142. data/site/architecture/robot-execution/index.html +3140 -0
  143. data/site/architecture/state-management/index.html +3498 -0
  144. data/site/assets/css/custom.css +56 -0
  145. data/site/assets/images/favicon.png +0 -0
  146. data/site/assets/images/robot_lab.jpg +0 -0
  147. data/site/assets/javascripts/bundle.79ae519e.min.js +16 -0
  148. data/site/assets/javascripts/bundle.79ae519e.min.js.map +7 -0
  149. data/site/assets/javascripts/lunr/min/lunr.ar.min.js +1 -0
  150. data/site/assets/javascripts/lunr/min/lunr.da.min.js +18 -0
  151. data/site/assets/javascripts/lunr/min/lunr.de.min.js +18 -0
  152. data/site/assets/javascripts/lunr/min/lunr.du.min.js +18 -0
  153. data/site/assets/javascripts/lunr/min/lunr.el.min.js +1 -0
  154. data/site/assets/javascripts/lunr/min/lunr.es.min.js +18 -0
  155. data/site/assets/javascripts/lunr/min/lunr.fi.min.js +18 -0
  156. data/site/assets/javascripts/lunr/min/lunr.fr.min.js +18 -0
  157. data/site/assets/javascripts/lunr/min/lunr.he.min.js +1 -0
  158. data/site/assets/javascripts/lunr/min/lunr.hi.min.js +1 -0
  159. data/site/assets/javascripts/lunr/min/lunr.hu.min.js +18 -0
  160. data/site/assets/javascripts/lunr/min/lunr.hy.min.js +1 -0
  161. data/site/assets/javascripts/lunr/min/lunr.it.min.js +18 -0
  162. data/site/assets/javascripts/lunr/min/lunr.ja.min.js +1 -0
  163. data/site/assets/javascripts/lunr/min/lunr.jp.min.js +1 -0
  164. data/site/assets/javascripts/lunr/min/lunr.kn.min.js +1 -0
  165. data/site/assets/javascripts/lunr/min/lunr.ko.min.js +1 -0
  166. data/site/assets/javascripts/lunr/min/lunr.multi.min.js +1 -0
  167. data/site/assets/javascripts/lunr/min/lunr.nl.min.js +18 -0
  168. data/site/assets/javascripts/lunr/min/lunr.no.min.js +18 -0
  169. data/site/assets/javascripts/lunr/min/lunr.pt.min.js +18 -0
  170. data/site/assets/javascripts/lunr/min/lunr.ro.min.js +18 -0
  171. data/site/assets/javascripts/lunr/min/lunr.ru.min.js +18 -0
  172. data/site/assets/javascripts/lunr/min/lunr.sa.min.js +1 -0
  173. data/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +1 -0
  174. data/site/assets/javascripts/lunr/min/lunr.sv.min.js +18 -0
  175. data/site/assets/javascripts/lunr/min/lunr.ta.min.js +1 -0
  176. data/site/assets/javascripts/lunr/min/lunr.te.min.js +1 -0
  177. data/site/assets/javascripts/lunr/min/lunr.th.min.js +1 -0
  178. data/site/assets/javascripts/lunr/min/lunr.tr.min.js +18 -0
  179. data/site/assets/javascripts/lunr/min/lunr.vi.min.js +1 -0
  180. data/site/assets/javascripts/lunr/min/lunr.zh.min.js +1 -0
  181. data/site/assets/javascripts/lunr/tinyseg.js +206 -0
  182. data/site/assets/javascripts/lunr/wordcut.js +6708 -0
  183. data/site/assets/javascripts/workers/search.2c215733.min.js +42 -0
  184. data/site/assets/javascripts/workers/search.2c215733.min.js.map +7 -0
  185. data/site/assets/stylesheets/main.484c7ddc.min.css +1 -0
  186. data/site/assets/stylesheets/main.484c7ddc.min.css.map +1 -0
  187. data/site/assets/stylesheets/palette.ab4e12ef.min.css +1 -0
  188. data/site/assets/stylesheets/palette.ab4e12ef.min.css.map +1 -0
  189. data/site/concepts/index.html +3455 -0
  190. data/site/examples/basic-chat/index.html +2880 -0
  191. data/site/examples/index.html +2907 -0
  192. data/site/examples/mcp-server/index.html +3018 -0
  193. data/site/examples/multi-robot-network/index.html +3131 -0
  194. data/site/examples/rails-application/index.html +3329 -0
  195. data/site/examples/tool-usage/index.html +3085 -0
  196. data/site/getting-started/configuration/index.html +3745 -0
  197. data/site/getting-started/index.html +2572 -0
  198. data/site/getting-started/installation/index.html +2981 -0
  199. data/site/getting-started/quick-start/index.html +2942 -0
  200. data/site/guides/building-robots/index.html +4290 -0
  201. data/site/guides/creating-networks/index.html +3858 -0
  202. data/site/guides/index.html +2586 -0
  203. data/site/guides/mcp-integration/index.html +3581 -0
  204. data/site/guides/memory/index.html +3586 -0
  205. data/site/guides/rails-integration/index.html +4019 -0
  206. data/site/guides/streaming/index.html +3157 -0
  207. data/site/guides/using-tools/index.html +3802 -0
  208. data/site/index.html +2671 -0
  209. data/site/search/search_index.json +1 -0
  210. data/site/sitemap.xml +183 -0
  211. data/site/sitemap.xml.gz +0 -0
  212. data/site/tags.json +1 -0
  213. data/temp.md +6 -0
  214. data/tool_manifest_plan.md +155 -0
  215. metadata +154 -92
  216. data/docs/examples/rails-application.md +0 -419
  217. data/docs/guides/ractor-parallelism.md +0 -364
  218. data/docs/guides/rails-integration.md +0 -681
  219. data/docs/superpowers/plans/2026-04-14-ractor-integration.md +0 -1538
  220. data/docs/superpowers/specs/2026-04-14-ractor-integration-design.md +0 -258
  221. data/lib/generators/robot_lab/install_generator.rb +0 -90
  222. data/lib/generators/robot_lab/job_generator.rb +0 -40
  223. data/lib/generators/robot_lab/robot_generator.rb +0 -55
  224. data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -42
  225. data/lib/generators/robot_lab/templates/job.rb.tt +0 -21
  226. data/lib/generators/robot_lab/templates/migration.rb.tt +0 -32
  227. data/lib/generators/robot_lab/templates/result_model.rb.tt +0 -52
  228. data/lib/generators/robot_lab/templates/robot.rb.tt +0 -31
  229. data/lib/generators/robot_lab/templates/robot_job.rb.tt +0 -18
  230. data/lib/generators/robot_lab/templates/robot_test.rb.tt +0 -34
  231. data/lib/generators/robot_lab/templates/routing_robot.rb.tt +0 -59
  232. data/lib/generators/robot_lab/templates/thread_model.rb.tt +0 -40
  233. data/lib/robot_lab/document_store.rb +0 -155
  234. data/lib/robot_lab/ractor_boundary.rb +0 -42
  235. data/lib/robot_lab/ractor_job.rb +0 -37
  236. data/lib/robot_lab/ractor_memory_proxy.rb +0 -85
  237. data/lib/robot_lab/ractor_network_scheduler.rb +0 -154
  238. data/lib/robot_lab/ractor_worker_pool.rb +0 -117
  239. data/lib/robot_lab/rails_integration/engine.rb +0 -29
  240. data/lib/robot_lab/rails_integration/job.rb +0 -158
  241. data/lib/robot_lab/rails_integration/railtie.rb +0 -51
  242. data/lib/robot_lab/rails_integration/turbo_stream_callbacks.rb +0 -72
@@ -0,0 +1 @@
1
+ {"config":{"lang":["en"],"separator":"[\\s\\-,:!=\\[\\]()\"`/]+|\\.(?!\\d)|&[lg]t;|(?!\\b)(?=[A-Z][a-z])","pipeline":["stopWordFilter"],"fields":{"title":{"boost":1000.0},"text":{"boost":1.0},"tags":{"boost":1000000.0}}},"docs":[{"location":"","title":"RobotLab","text":"<p>[!CAUTION] This gem is under active development. APIs and features may change without notice. See the CHANGELOG for details.</p> \"Build robots. Solve problems.\" RobotLab is a Ruby gem that enables you to build sophisticated AI applications using multiple specialized robots (LLM agents) that work together to accomplish complex tasks. Each robot is backed by a persistent LLM chat, configured with keyword arguments, and run with a simple positional message. Robots can be orchestrated through networks with task-based pipelines, share information through a reactive memory system, and connect to external tools via the Model Context Protocol (MCP)."},{"location":"#key-features","title":"Key Features","text":"<ul> <li> <p> Multi-Robot Architecture</p> <p>Build applications with multiple specialized AI agents, each inheriting from <code>RubyLLM::Agent</code> with persistent chat and memory.</p> <p> Learn more</p> </li> <li> <p> Network Orchestration</p> <p>Connect robots in task-based pipelines using SimpleFlow with sequential, parallel, and conditional execution.</p> <p> Creating Networks</p> </li> <li> <p> Extensible Tools</p> <p>Give robots tools via <code>RubyLLM::Tool</code> subclasses or <code>RobotLab::Tool.new</code> with block handlers.</p> <p> Using Tools</p> </li> <li> <p> MCP Integration</p> <p>Connect to Model Context Protocol servers to extend robot capabilities with external tools.</p> <p> MCP Guide</p> </li> <li> <p> Message Bus</p> <p>Enable bidirectional, cyclic communication between robots via TypedBus for negotiation loops and convergence patterns.</p> <p> Message Bus</p> </li> <li> <p> Reactive Memory</p> <p>Robots share data through a reactive key-value memory system with subscriptions, blocking reads, and optional Redis backend.</p> <p> Memory System</p> </li> </ul>"},{"location":"#quick-example","title":"Quick Example","text":"<pre><code>require \"robot_lab\"\n\n# Configuration is automatic via environment variables, YAML files, or defaults.\n# Set API keys via env vars:\n# ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...\n#\n# Or place a config file at ~/.config/robot_lab/config.yml\n# Access config values: RobotLab.config.ruby_llm.model #=&gt; \"claude-sonnet-4\"\n\n# Create a robot with keyword arguments\nrobot = RobotLab.build(\n name: \"assistant\",\n system_prompt: \"You are a helpful assistant. Answer questions clearly and concisely.\",\n model: \"claude-sonnet-4\"\n)\n\n# Run the robot with a positional string argument\nresult = robot.run(\"What is the capital of France?\")\n\nputs result.last_text_content\n# =&gt; \"The capital of France is Paris.\"\n\n# Memory persists across runs\nrobot.run(\"Remember that my favorite color is blue.\")\nresult = robot.run(\"What is my favorite color?\")\nputs result.last_text_content\n# =&gt; \"Your favorite color is blue.\"\n\n# Chaining configuration\nrobot.with_instructions(\"Be extra concise.\").with_temperature(0.3).run(\"Explain Ruby in one sentence.\")\n</code></pre>"},{"location":"#supported-llm-providers","title":"Supported LLM Providers","text":"<p>RobotLab supports multiple LLM providers through the ruby_llm library:</p> Provider Models Anthropic Claude Opus 4, Claude Sonnet 4, Claude Haiku 3.5 OpenAI GPT-4o, GPT-4, o1, o3 Google Gemini 2.5 Pro, Gemini 2.5 Flash DeepSeek DeepSeek V3, DeepSeek R1 AWS Bedrock Claude models via AWS Bedrock Google Vertex AI Gemini models via Vertex AI Ollama Local models via Ollama OpenRouter Multi-provider routing Mistral Mistral Large, Mistral Medium xAI Grok models"},{"location":"#installation","title":"Installation","text":"<p>Add RobotLab to your Gemfile:</p> <pre><code>gem \"robot_lab\"\n</code></pre> <p>Or install directly:</p> <pre><code>gem install robot_lab\n</code></pre> <p> Full Installation Guide</p>"},{"location":"#next-steps","title":"Next Steps","text":"<ul> <li> <p> Quick Start</p> <p>Get up and running in 5 minutes</p> </li> <li> <p> Concepts</p> <p>Understand the core concepts</p> </li> <li> <p> Examples</p> <p>See RobotLab in action</p> </li> <li> <p> API Reference</p> <p>Detailed API documentation</p> </li> </ul>"},{"location":"#license","title":"License","text":"<p>RobotLab is released under the MIT License.</p>"},{"location":"concepts/","title":"Core Concepts","text":"<p>Understanding the fundamental concepts in RobotLab will help you build effective AI applications.</p>"},{"location":"concepts/#robot","title":"Robot","text":"<p>A Robot is an LLM-powered agent that inherits from <code>RubyLLM::Agent</code>. 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 <code>RobotLab.build</code> factory method.</p> <p>Each robot has:</p> <ul> <li>Name: A unique identifier (auto-generated if omitted)</li> <li>Template: A <code>.md</code> file with YAML front matter managed by prompt_manager, referenced by symbol</li> <li>System Prompt: Inline instructions (can be used alone or combined with a template)</li> <li>Model: The LLM model to use (defaults to <code>RobotLab.config.ruby_llm.model</code>)</li> <li>Skills: Composable template behaviors prepended before the main template</li> <li>Local Tools: <code>RubyLLM::Tool</code> subclasses or <code>RobotLab::Tool</code> instances (with automatic error handling)</li> <li>Streaming: Real-time content via stored <code>on_content</code> callback or per-call block</li> <li>Memory: Persistent key-value store across runs</li> </ul> <pre><code># Robot with template (references prompts/support.md)\nrobot = RobotLab.build(\n name: \"support_agent\",\n template: :support,\n context: { tone: \"friendly\", department: \"billing\" },\n local_tools: [OrderLookup, RefundProcessor],\n model: \"claude-sonnet-4\"\n)\n\n# Robot with inline system prompt\nrobot = RobotLab.build(\n name: \"helper\",\n system_prompt: \"You are a friendly customer support agent.\"\n)\n\n# Bare robot configured via chaining\nrobot = RobotLab.build(name: \"bot\")\nrobot.with_instructions(\"Be concise.\").with_temperature(0.3).run(\"Hello\")\n</code></pre> <p>The primary method is <code>robot.run(\"message\")</code>, which takes a positional string argument and returns a <code>RobotResult</code>:</p> <pre><code>result = robot.run(\"What is 2 + 2?\")\nputs result.last_text_content # =&gt; \"4\"\n</code></pre> <p>Standalone robots persist their conversation history and memory across runs:</p> <pre><code>robot.run(\"My name is Alice.\")\nresult = robot.run(\"What is my name?\")\nputs result.last_text_content # =&gt; \"Your name is Alice.\"\n</code></pre>"},{"location":"concepts/#configuration","title":"Configuration","text":"<p>RobotLab uses <code>MywayConfig</code> for configuration. There is no <code>RobotLab.configure</code> block. Instead, configuration is loaded automatically from multiple sources in priority order:</p> <ol> <li>Bundled defaults (<code>lib/robot_lab/config/defaults.yml</code>)</li> <li>Environment-specific overrides (development, test, production)</li> <li>XDG user config (<code>~/.config/robot_lab/config.yml</code>)</li> <li>Project config (<code>./config/robot_lab.yml</code>)</li> <li>Environment variables (<code>ROBOT_LAB_*</code> prefix)</li> </ol> <pre><code># Access configuration values\nRobotLab.config.ruby_llm.model #=&gt; \"claude-sonnet-4\"\nRobotLab.config.ruby_llm.request_timeout #=&gt; 120\n\n# Set API keys via environment variables\n# ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...\n# ROBOT_LAB_RUBY_LLM__OPENAI_API_KEY=sk-...\n\n# Reload configuration\nRobotLab.reload_config!\n</code></pre>"},{"location":"concepts/#network","title":"Network","text":"<p>A Network is a collection of robots orchestrated using SimpleFlow pipelines. Networks provide:</p> <ul> <li>Task-Based Orchestration: Define tasks with dependencies and routing</li> <li>Parallel Execution: Tasks with the same dependencies run concurrently</li> <li>Optional Task Activation: Dynamic routing based on robot output</li> <li>Per-Task Configuration: Each task can have its own context, tools, and MCP servers</li> <li>Shared Memory: All robots in a network share a reactive memory instance</li> </ul> <pre><code>network = RobotLab.create_network(name: \"customer_service\") do\n task :classifier, classifier_robot, depends_on: :none\n task :billing, billing_robot,\n context: { department: \"billing\" },\n depends_on: :optional\n task :technical, technical_robot,\n context: { department: \"technical\" },\n depends_on: :optional\nend\n\nresult = network.run(message: \"I was charged twice for my subscription.\")\n</code></pre>"},{"location":"concepts/#task","title":"Task","text":"<p>A Task wraps a robot for use in a network pipeline with per-task configuration:</p> <ul> <li>Context: Task-specific context deep-merged with network run params</li> <li>MCP: MCP servers available to this task (<code>:none</code>, <code>:inherit</code>, or array)</li> <li>Tools: Tools available to this task (<code>:none</code>, <code>:inherit</code>, or array)</li> <li>Memory: Task-specific memory</li> <li>Dependencies: <code>:none</code>, <code>[:task1, :task2]</code>, or <code>:optional</code></li> </ul> <pre><code>task :billing, billing_robot,\n context: { department: \"billing\", escalation_level: 2 },\n tools: [RefundTool, InvoiceTool],\n depends_on: :optional\n</code></pre>"},{"location":"concepts/#simpleflowresult","title":"SimpleFlow::Result","text":"<p>Networks use <code>SimpleFlow::Result</code> for data flow between tasks:</p> <pre><code>result.value # Current task's output (RobotResult)\nresult.context # Accumulated context from all tasks\nresult.halted? # Whether execution stopped early\nresult.continued? # Whether execution continues\n</code></pre>"},{"location":"concepts/#result-methods","title":"Result Methods","text":"Method Purpose <code>continue(value)</code> Continue to next tasks <code>halt(value)</code> Stop pipeline execution <code>with_context(key, val)</code> Add data to context <code>activate(task_name)</code> Enable optional task"},{"location":"concepts/#tool","title":"Tool","text":"<p>Tools give robots the ability to interact with external systems. <code>RobotLab::Tool</code> extends <code>RubyLLM::Tool</code> with graceful error handling \u2014 if <code>execute</code> raises a <code>StandardError</code>, the error is caught and returned as a plain-text string (<code>\"Error (tool_name): message\"</code>) so the LLM can reason about it. Critical tools can opt out with <code>self.raise_on_error = true</code>.</p> <p>There are two patterns for defining tools:</p>"},{"location":"concepts/#rubyllmtool-subclass-preferred","title":"RubyLLM::Tool Subclass (Preferred)","text":"<pre><code>class Calculator &lt; RubyLLM::Tool\n description \"Performs basic arithmetic operations\"\n\n param :operation, type: \"string\", desc: \"The operation (add, subtract, multiply, divide)\"\n param :a, type: \"number\", desc: \"First operand\"\n param :b, type: \"number\", desc: \"Second operand\"\n\n def execute(operation:, a:, b:)\n case operation\n when \"add\" then a + b\n when \"subtract\" then a - b\n when \"multiply\" then a * b\n when \"divide\" then a.to_f / b\n else \"Unknown operation: #{operation}\"\n end\n end\nend\n\nrobot = RobotLab.build(\n name: \"math_bot\",\n system_prompt: \"You can do math.\",\n local_tools: [Calculator]\n)\n</code></pre>"},{"location":"concepts/#robotlabtoolcreate-factory","title":"RobotLab::Tool.create Factory","text":"<pre><code>tool = RobotLab::Tool.create(\n name: \"get_weather\",\n description: \"Get current weather for a location\",\n parameters: {\n type: \"object\",\n properties: {\n location: { type: \"string\", description: \"City name\" }\n },\n required: [\"location\"]\n }\n) { |args| WeatherService.current(args[:location]) }\n</code></pre>"},{"location":"concepts/#robotresult","title":"RobotResult","text":"<p><code>RobotResult</code> captures the output of a single <code>robot.run(...)</code> call:</p> <pre><code>result = robot.run(\"Hello!\")\n\nresult.last_text_content # =&gt; \"Hi there!\" (String or nil)\nresult.output # =&gt; [TextMessage, ...] array of output messages\nresult.tool_calls # =&gt; [] array of tool call results\nresult.robot_name # =&gt; \"assistant\"\nresult.stop_reason # =&gt; \"end_turn\" or nil\nresult.has_tool_calls? # =&gt; false\nresult.checksum # =&gt; \"a1b2c3d4...\" (for dedup)\n</code></pre>"},{"location":"concepts/#memory","title":"Memory","text":"<p>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.</p> <pre><code># Standalone robot with inherent memory\nrobot = RobotLab.build(name: \"assistant\", system_prompt: \"You are helpful.\")\nrobot.run(\"My name is Alice\")\nrobot.run(\"What's my name?\") # Memory persists across runs\n\n# Access robot's memory directly\nrobot.memory[:user_id] = 123\nrobot.memory.data[:category] = \"billing\"\nrobot.memory.data.category # =&gt; \"billing\" (method-style access)\n\n# Runtime memory injection\nrobot.run(\"Help me\", memory: { session_id: \"abc123\" })\n\n# Reset memory\nrobot.reset_memory\n</code></pre>"},{"location":"concepts/#reserved-memory-keys","title":"Reserved Memory Keys","text":"Key Purpose <code>:data</code> Runtime data (StateProxy for method-style access) <code>:results</code> Accumulated robot results <code>:messages</code> Conversation history <code>:session_id</code> Session identifier for history persistence <code>:cache</code> Semantic cache instance (RubyLLM::SemanticCache)"},{"location":"concepts/#reactive-memory-in-networks","title":"Reactive Memory in Networks","text":"<p>In a network, shared memory supports pub/sub semantics for inter-robot communication:</p> <pre><code># Robot A writes to shared memory\nnetwork.memory.set(:sentiment, { score: 0.8 })\n\n# Robot B reads (blocking until available)\nresult = network.memory.get(:sentiment, wait: true)\nresult = network.memory.get(:sentiment, wait: 30) # timeout in seconds\n\n# Multiple keys\nresults = network.memory.get(:sentiment, :entities, :keywords, wait: 60)\n\n# Subscribe to changes\nnetwork.memory.subscribe(:status) do |change|\n puts \"#{change.key} changed by #{change.writer}: #{change.value}\"\nend\n</code></pre>"},{"location":"concepts/#mcp-model-context-protocol","title":"MCP (Model Context Protocol)","text":"<p>MCP allows robots to connect to external tool servers:</p> <pre><code>robot = RobotLab.build(\n name: \"developer\",\n system_prompt: \"You are a developer assistant.\",\n mcp: [\n { name: \"filesystem\", transport: { type: \"stdio\", command: \"mcp-server-filesystem\" } },\n { name: \"github\", transport: { type: \"stdio\", command: \"mcp-server-github\" } }\n ]\n)\n</code></pre> <p>MCP configuration follows a hierarchical resolution: <code>runtime &gt; robot &gt; network &gt; global config</code>. Values can be <code>:none</code>, <code>:inherit</code>, or explicit arrays.</p>"},{"location":"concepts/#execution-flow","title":"Execution Flow","text":"<pre><code>sequenceDiagram\n participant User\n participant Network\n participant Pipeline\n participant Task\n participant Robot\n participant LLM\n participant Tool\n\n User-&gt;&gt;Network: run(message: \"...\", **context)\n Network-&gt;&gt;Pipeline: call_parallel(initial_result)\n Pipeline-&gt;&gt;Task: call(result)\n Task-&gt;&gt;Robot: call(enhanced_result)\n Robot-&gt;&gt;Robot: extract_run_context(result)\n Robot-&gt;&gt;LLM: ask(message)\n\n alt Tool Call\n LLM--&gt;&gt;Robot: tool_call\n Robot-&gt;&gt;Tool: execute(params)\n Tool--&gt;&gt;Robot: result\n Robot-&gt;&gt;LLM: continue with result\n end\n\n LLM--&gt;&gt;Robot: response\n Robot--&gt;&gt;Task: RobotResult\n Task--&gt;&gt;Pipeline: result.continue(robot_result)\n\n alt Optional Task Activated\n Pipeline-&gt;&gt;Task: call activated task\n end\n\n Pipeline--&gt;&gt;Network: final result\n Network--&gt;&gt;User: SimpleFlow::Result</code></pre>"},{"location":"concepts/#conditional-routing-with-classifierrobot","title":"Conditional Routing with ClassifierRobot","text":"<p>Use a custom Robot subclass to implement intelligent routing. Override <code>call(result)</code> to inspect the LLM output and activate optional tasks:</p> <pre><code>class ClassifierRobot &lt; RobotLab::Robot\n def call(result)\n # Extract context and message from the pipeline result\n context = extract_run_context(result)\n message = context.delete(:message)\n\n # Run the robot to classify the input\n robot_result = run(message, **context)\n\n new_result = result\n .with_context(@name.to_sym, robot_result)\n .continue(robot_result)\n\n # Activate the appropriate specialist based on classification\n category = robot_result.last_text_content.to_s.strip.downcase\n case category\n when /billing/ then new_result.activate(:billing)\n when /technical/ then new_result.activate(:technical)\n else new_result.activate(:general)\n end\n end\nend\n\n# Build the classifier (uses a .md template with YAML front matter)\nclassifier = ClassifierRobot.new(\n name: \"classifier\",\n template: :classifier,\n model: \"claude-sonnet-4\"\n)\n\n# Build specialist robots\nbilling_robot = RobotLab.build(name: \"billing\", template: :billing)\ntechnical_robot = RobotLab.build(name: \"technical\", template: :technical)\ngeneral_robot = RobotLab.build(name: \"general\", template: :general)\n\n# Create network with optional task routing\nnetwork = RobotLab.create_network(name: \"support_network\") do\n task :classifier, classifier, depends_on: :none\n task :billing, billing_robot, depends_on: :optional\n task :technical, technical_robot, depends_on: :optional\n task :general, general_robot, depends_on: :optional\nend\n\nresult = network.run(message: \"I was charged twice for my subscription.\")\nputs result.value.last_text_content\n</code></pre>"},{"location":"concepts/#message-bus","title":"Message Bus","text":"<p>The Message Bus enables bidirectional, cyclic communication between robots via <code>typed_bus</code>. While Networks enforce DAG-based execution, the bus supports negotiation loops, convergence patterns, and multi-turn dialogues.</p> <pre><code>bus = TypedBus::MessageBus.new\n\nbob = RobotLab.build(name: \"bob\", system_prompt: \"You tell jokes.\", bus: bus)\nalice = RobotLab.build(name: \"alice\", system_prompt: \"You evaluate jokes.\", bus: bus)\n\n# Register handlers\nbob.on_message do |message|\n joke = bob.run(message.content.to_s).last_text_content\n bob.send_reply(to: message.from.to_sym, content: joke, in_reply_to: message.key)\nend\n\nalice.on_message do |message|\n verdict = alice.run(\"Is this funny? #{message.content}\").last_text_content\n # Send another request if not satisfied\n alice.send_message(to: :bob, content: \"Try again.\") unless verdict.start_with?(\"FUNNY\")\nend\n\n# Start the conversation\nalice.send_message(to: :bob, content: \"Tell me a robot joke.\")\n</code></pre> <p>Key features:</p> <ul> <li>Typed channels \u2014 only <code>RobotMessage</code> objects accepted per channel</li> <li>Auto-ACK \u2014 1-arg <code>on_message</code> blocks auto-acknowledge; 2-arg blocks give manual control</li> <li>Reply correlation \u2014 <code>send_reply(to:, content:, in_reply_to:)</code> tracks threads via <code>in_reply_to</code></li> <li>Independent of Network \u2014 bus works without a Network pipeline</li> </ul>"},{"location":"concepts/#dynamic-spawning","title":"Dynamic Spawning","text":"<p>Robots can create new robots at runtime using <code>spawn</code>. The bus is created lazily:</p> <pre><code>dispatcher = RobotLab.build(name: \"dispatcher\", system_prompt: \"You delegate work.\")\n\n# spawn creates a child on the same bus (bus created automatically)\nhelper = dispatcher.spawn(name: \"helper\", system_prompt: \"You answer questions.\")\nanswer = helper.run(\"What is 2+2?\").last_text_content\nhelper.send_message(to: :dispatcher, content: answer)\n</code></pre> <p>Robots can also join a bus after creation with <code>with_bus</code>:</p> <pre><code>bot = RobotLab.build(name: \"latecomer\", system_prompt: \"Hello.\")\nbot.with_bus(existing_bus)\n</code></pre> <p>Multiple robots with the same name enable fan-out \u2014 messages sent to that name are delivered to all subscribers.</p>"},{"location":"concepts/#templates","title":"Templates","text":"<p>Templates are <code>.md</code> files with YAML front matter, managed by the prompt_manager gem. They live in the configured template path (default: <code>./prompts/</code> or <code>app/prompts/</code> in Rails).</p> <pre><code>---\ndescription: Customer support classifier\nmodel: claude-sonnet-4\ntemperature: 0.3\n---\nYou are a request classifier. Analyze the user's request and classify it\nas either \"billing\", \"technical\", or \"general\".\n\nRespond with ONLY the category name, nothing else.\n</code></pre> <p>Reference templates by symbol when building robots:</p> <pre><code>robot = RobotLab.build(\n name: \"classifier\",\n template: :classifier, # loads prompts/classifier.md\n context: { tone: \"professional\" } # variables passed to the template\n)\n</code></pre>"},{"location":"concepts/#front-matter-keys","title":"Front Matter Keys","text":"<p>Templates support two categories of front matter keys:</p> <p>LLM Config: <code>model</code>, <code>temperature</code>, <code>top_p</code>, <code>top_k</code>, <code>max_tokens</code>, <code>presence_penalty</code>, <code>frequency_penalty</code>, <code>stop</code> \u2014 applied to the robot's chat configuration.</p> <p>Robot Extras: <code>robot_name</code>, <code>description</code>, <code>tools</code>, <code>mcp</code>, <code>skills</code> \u2014 applied to the robot's identity and capabilities. These make templates self-contained: reading the <code>.md</code> file tells you everything about the robot.</p> <pre><code>---\ndescription: GitHub assistant with MCP tool access\nrobot_name: github_bot\ntools:\n - CodeSearchTool\nmcp:\n - name: github\n transport: stdio\n command: npx\n args: [\"-y\", \"@modelcontextprotocol/server-github\"]\nmodel: claude-sonnet-4\n---\nYou are a GitHub assistant. Use available tools to help with repository tasks.\n</code></pre> <pre><code># Template provides everything \u2014 minimal constructor\nrobot = RobotLab.build(template: :github_assistant)\n</code></pre> <p>Constructor-provided values (<code>local_tools:</code>, <code>mcp:</code>, <code>name:</code>, <code>description:</code>) always take precedence over front matter values.</p>"},{"location":"concepts/#next-steps","title":"Next Steps","text":"<ul> <li>Quick Start Guide - Build your first robot</li> <li>Building Robots - Detailed robot creation guide</li> <li>Creating Networks - Network orchestration patterns</li> </ul>"},{"location":"api/","title":"API Reference","text":"<p>Complete API documentation for RobotLab.</p>"},{"location":"api/#core-classes","title":"Core Classes","text":"<p>The fundamental building blocks of RobotLab:</p> Class Description Robot LLM-powered agent with personality and tools Network Orchestrates multiple robots Memory Reactive key-value store for sharing data Tool Custom function robots can call"},{"location":"api/#messages","title":"Messages","text":"<p>Message types for LLM communication:</p> Class Description UserMessage User input with metadata TextMessage Text message with role ToolCallMessage Tool execution request ToolResultMessage Tool execution result"},{"location":"api/#mcp-model-context-protocol","title":"MCP (Model Context Protocol)","text":"<p>Connect to external tool servers:</p> Class Description Client MCP server connection Server Server configuration Transports Connection transports"},{"location":"api/#streaming","title":"Streaming","text":"<p>Real-time response streaming:</p> Class Description Context Streaming context Events Event utilities"},{"location":"api/#module-methods","title":"Module Methods","text":""},{"location":"api/#robotlab","title":"RobotLab","text":"<pre><code># Configuration\nRobotLab.config # =&gt; Config instance\nRobotLab.reload_config! # =&gt; reload from all sources\n\n# Building\nRobotLab.build(name:, template:, system_prompt:, context:, **options)\nRobotLab.create_network(name:, concurrency:) { ... }\nRobotLab.create_memory(data:, enable_cache:, **options)\n</code></pre> <p>See individual class documentation for detailed method references.</p>"},{"location":"api/core/","title":"Core Classes","text":"<p>The fundamental classes that power RobotLab.</p>"},{"location":"api/core/#overview","title":"Overview","text":"<pre><code>classDiagram\n class Robot {\n +name: String\n +description: String\n +model: String\n +template: String\n +tools: Array~Tool~\n +run(message) Message\n }\n\n class Network {\n +name: String\n +robots: Hash\n +config: RunConfig\n +run(message)\n }\n\n class RunConfig {\n +model: String\n +temperature: Float\n +merge(other) RunConfig\n +apply_to(chat)\n }\n\n class Tool {\n +name: String\n +description: String\n +parameters: Hash\n +handler: Proc\n }\n\n class Memory {\n +set(key, value)\n +get(key)\n +delete(key)\n +data: StateProxy\n +results: Array\n +messages: Array\n +session_id: String\n }\n\n class RobotMessage {\n +id: Integer\n +from: String\n +content: String\n +in_reply_to: String\n +key()\n +reply?()\n }\n\n Network --&gt; Robot : contains\n Network --&gt; RunConfig : uses\n Robot --&gt; RunConfig : uses\n Robot --&gt; Tool : has\n Robot --&gt; Memory : uses\n Network --&gt; Memory : uses\n Robot ..&gt; RobotMessage : sends/receives</code></pre>"},{"location":"api/core/#classes","title":"Classes","text":"Class Purpose Robot LLM agent with personality, tools, and model configuration Network Container for robots with routing and orchestration RunConfig Shared configuration for LLM, tools, callbacks, and infrastructure Tool Callable function with parameters and handler AskUser Built-in tool for terminal-based user interaction Memory Reactive key-value store for sharing data RobotMessage Typed envelope for bus-based inter-robot communication"},{"location":"api/core/#quick-examples","title":"Quick Examples","text":""},{"location":"api/core/#robot","title":"Robot","text":"<pre><code>robot = RobotLab.build(\n name: \"assistant\",\n model: \"claude-sonnet-4\",\n system_prompt: \"You are helpful.\",\n local_tools: [greet_tool]\n)\n\nresult = robot.run(\"Hello!\")\n</code></pre>"},{"location":"api/core/#network","title":"Network","text":"<pre><code>network = RobotLab.create_network(name: \"my_network\") do\n step :analyzer, analyzer_robot, depends_on: :none\n step :writer, writer_robot, depends_on: [:analyzer]\nend\n\nresult = network.run(message: \"Process this\")\n</code></pre>"},{"location":"api/core/#memory","title":"Memory","text":"<pre><code>memory = RobotLab.create_memory(data: { user_id: \"123\" })\nmemory.set(:category, \"billing\")\nmemory.get(:category) # =&gt; \"billing\"\n</code></pre>"},{"location":"api/core/#tool","title":"Tool","text":"<pre><code>tool = RobotLab::Tool.create(\n name: \"get_time\",\n description: \"Get current time\"\n) { |**_| Time.now.to_s }\n</code></pre>"},{"location":"api/core/memory/","title":"Memory","text":"<p>Reactive key-value store for sharing data between robots.</p>"},{"location":"api/core/memory/#class-robotlabmemory","title":"Class: <code>RobotLab::Memory</code>","text":"<pre><code>memory = robot.memory\n\nmemory.set(:key, \"value\")\nvalue = memory.get(:key)\n</code></pre>"},{"location":"api/core/memory/#constants","title":"Constants","text":""},{"location":"api/core/memory/#reserved_keys","title":"RESERVED_KEYS","text":"<pre><code>Memory::RESERVED_KEYS # =&gt; [:data, :results, :messages, :session_id, :cache]\n</code></pre> <p>Reserved keys with special accessors and behavior.</p>"},{"location":"api/core/memory/#constructor","title":"Constructor","text":"<pre><code>memory = Memory.new(\n data: {},\n results: [],\n messages: [],\n session_id: nil,\n backend: :auto,\n enable_cache: true,\n network_name: nil\n)\n</code></pre> <p>Parameters:</p> Name Type Default Description <code>data</code> <code>Hash</code> <code>{}</code> Initial runtime data <code>results</code> <code>Array</code> <code>[]</code> Pre-loaded robot results <code>messages</code> <code>Array</code> <code>[]</code> Pre-loaded conversation messages <code>session_id</code> <code>String, nil</code> <code>nil</code> Session identifier <code>backend</code> <code>Symbol</code> <code>:auto</code> Storage backend (<code>:auto</code>, <code>:redis</code>, <code>:hash</code>) <code>enable_cache</code> <code>Boolean</code> <code>true</code> Whether to enable semantic caching <code>network_name</code> <code>String, nil</code> <code>nil</code> Network this memory belongs to"},{"location":"api/core/memory/#factory-method","title":"Factory Method","text":"<pre><code>memory = RobotLab.create_memory(data: { user_id: 123 })\n</code></pre>"},{"location":"api/core/memory/#methods","title":"Methods","text":""},{"location":"api/core/memory/#set","title":"set","text":"<pre><code>memory.set(:key, value)\n</code></pre> <p>Store a value and notify subscribers asynchronously.</p> <p>Parameters:</p> Name Type Description <code>key</code> <code>Symbol</code>, <code>String</code> Storage key <code>value</code> <code>Object</code> Value to store"},{"location":"api/core/memory/#get","title":"get","text":"<pre><code>memory.get(:key) # =&gt; value or nil\nmemory.get(:key, wait: true) # Block until available\nmemory.get(:key, wait: 30) # Block up to 30 seconds\nmemory.get(:a, :b, :c, wait: 60) # Multiple keys, returns Hash\n</code></pre> <p>Retrieve one or more values, optionally waiting until they exist.</p> <p>Parameters:</p> Name Type Description <code>keys</code> <code>Symbol</code>, <code>String</code> One or more keys to retrieve <code>wait</code> <code>Boolean</code>, <code>Numeric</code> <code>false</code>: immediate, <code>true</code>: block, <code>Numeric</code>: timeout <p>Returns: Single value for one key, <code>Hash</code> for multiple keys.</p> <p>Raises: <code>AwaitTimeout</code> if timeout expires.</p>"},{"location":"api/core/memory/#key","title":"key?","text":"<pre><code>memory.key?(:key) # =&gt; Boolean\n</code></pre> <p>Check if key exists. Aliases: <code>has_key?</code>, <code>include?</code>.</p>"},{"location":"api/core/memory/#delete","title":"delete","text":"<pre><code>memory.delete(:key) # =&gt; deleted value\n</code></pre> <p>Remove a key. Cannot delete reserved keys.</p>"},{"location":"api/core/memory/#keys","title":"keys","text":"<pre><code>memory.keys # =&gt; Array&lt;Symbol&gt;\n</code></pre> <p>Get all non-reserved keys.</p>"},{"location":"api/core/memory/#clear","title":"clear","text":"<pre><code>memory.clear\n</code></pre> <p>Clear all non-reserved keys.</p>"},{"location":"api/core/memory/#reset","title":"reset","text":"<pre><code>memory.reset\n</code></pre> <p>Reset memory to initial state (clears everything including reserved keys, preserves cache).</p>"},{"location":"api/core/memory/#subscribe","title":"subscribe","text":"<pre><code>sub_id = memory.subscribe(:key1, :key2) do |change|\n puts \"#{change.key} changed: #{change.value}\"\nend\n</code></pre> <p>Subscribe to changes on one or more keys. Callback receives a <code>MemoryChange</code> object.</p> <p>MemoryChange attributes:</p> Attribute Type Description <code>key</code> <code>Symbol</code> The changed key <code>value</code> <code>Object</code> New value <code>previous</code> <code>Object</code> Previous value <code>writer</code> <code>String, nil</code> Name of robot that wrote <code>network_name</code> <code>String, nil</code> Network name <code>timestamp</code> <code>Time</code> When the change occurred <code>created?</code> <code>Boolean</code> Previous was nil <code>updated?</code> <code>Boolean</code> Previous was not nil <code>deleted?</code> <code>Boolean</code> New value is nil"},{"location":"api/core/memory/#subscribe_pattern","title":"subscribe_pattern","text":"<pre><code>sub_id = memory.subscribe_pattern(\"analysis:*\") do |change|\n puts \"Analysis key #{change.key} updated\"\nend\n</code></pre> <p>Subscribe to keys matching a glob pattern (<code>*</code> and <code>?</code> supported).</p>"},{"location":"api/core/memory/#unsubscribe","title":"unsubscribe","text":"<pre><code>memory.unsubscribe(sub_id) # =&gt; Boolean\n</code></pre> <p>Remove a subscription by its ID.</p>"},{"location":"api/core/memory/#merge","title":"merge!","text":"<pre><code>memory.merge!(key1: \"value1\", key2: \"value2\")\n</code></pre> <p>Merge multiple key-value pairs into memory.</p>"},{"location":"api/core/memory/#reserved-key-accessors","title":"Reserved Key Accessors","text":""},{"location":"api/core/memory/#data","title":"data","text":"<pre><code>memory.data # =&gt; StateProxy\nmemory.data[:user_id] # Hash access\nmemory.data.user_id # Method access\nmemory.data[:status] = \"active\"\n</code></pre> <p>Runtime data accessed through <code>StateProxy</code> for method-style access.</p>"},{"location":"api/core/memory/#results","title":"results","text":"<pre><code>memory.results # =&gt; Array&lt;RobotResult&gt;\n</code></pre> <p>Accumulated robot results (returns a copy).</p>"},{"location":"api/core/memory/#messages","title":"messages","text":"<pre><code>memory.messages # =&gt; Array&lt;Message&gt;\n</code></pre> <p>Conversation messages (returns a copy).</p>"},{"location":"api/core/memory/#session_id","title":"session_id","text":"<pre><code>memory.session_id # =&gt; String | nil\nmemory.session_id = \"abc\" # Set session identifier\n</code></pre>"},{"location":"api/core/memory/#cache","title":"cache","text":"<pre><code>memory.cache # =&gt; RubyLLM::SemanticCache\n</code></pre> <p>Semantic cache module (read-only after initialization).</p>"},{"location":"api/core/memory/#serialization","title":"Serialization","text":""},{"location":"api/core/memory/#to_h","title":"to_h","text":"<pre><code>memory.to_h\n# =&gt; { data: {...}, results: [...], messages: [...], session_id: \"...\", custom: {...} }\n</code></pre>"},{"location":"api/core/memory/#to_json","title":"to_json","text":"<pre><code>memory.to_json # =&gt; String\n</code></pre>"},{"location":"api/core/memory/#from_hash","title":"from_hash","text":"<pre><code>memory = Memory.from_hash(hash)\n</code></pre> <p>Reconstruct memory from a hash.</p>"},{"location":"api/core/memory/#clone","title":"clone","text":"<pre><code>new_memory = memory.clone\n</code></pre> <p>Deep copy with fresh subscriptions (cache and network_name preserved).</p>"},{"location":"api/core/memory/#examples","title":"Examples","text":""},{"location":"api/core/memory/#basic-usage","title":"Basic Usage","text":"<pre><code>robot.memory.set(:user_name, \"Alice\")\nrobot.memory.set(:order_count, 5)\n\nname = robot.memory.get(:user_name) # =&gt; \"Alice\"\ncount = robot.memory.get(:order_count) # =&gt; 5\n</code></pre>"},{"location":"api/core/memory/#bracket-access","title":"Bracket Access","text":"<pre><code>robot.memory[:user_id] = 123\nrobot.memory[:user_id] # =&gt; 123\n</code></pre>"},{"location":"api/core/memory/#storing-objects","title":"Storing Objects","text":"<pre><code>robot.memory.set(:user, {\n id: 123,\n name: \"Alice\",\n plan: \"pro\"\n})\n\nuser = robot.memory.get(:user)\nuser[:plan] # =&gt; \"pro\"\n</code></pre>"},{"location":"api/core/memory/#blocking-reads-network-parallel-execution","title":"Blocking Reads (Network Parallel Execution)","text":"<pre><code># In robot A (writer)\nnetwork.memory.set(:sentiment, { score: 0.8, confidence: 0.95 })\n\n# In robot B (reader, may run concurrently)\nresult = network.memory.get(:sentiment, wait: true) # Block indefinitely\nresult = network.memory.get(:sentiment, wait: 30) # Block up to 30s\n\n# Multiple keys with timeout\nresults = network.memory.get(:sentiment, :entities, :keywords, wait: 60)\n# =&gt; { sentiment: {...}, entities: [...], keywords: [...] }\n</code></pre>"},{"location":"api/core/memory/#reactive-subscriptions","title":"Reactive Subscriptions","text":"<pre><code># Subscribe to a key\nmemory.subscribe(:raw_data) do |change|\n enriched = enrich(change.value)\n memory.set(:enriched, enriched)\nend\n\n# Subscribe with pattern\nmemory.subscribe_pattern(\"user:*\") do |change|\n puts \"User key #{change.key} updated by #{change.writer}\"\nend\n</code></pre>"},{"location":"api/core/memory/#cross-robot-communication-via-network-memory","title":"Cross-Robot Communication via Network Memory","text":"<pre><code># In classifier robot\nnetwork.memory.set(:intent, \"billing\")\nnetwork.memory.set(:entities, [\"order\", \"refund\"])\n\n# In handler robot\nintent = network.memory.get(:intent)\nentities = network.memory.get(:entities)\n</code></pre>"},{"location":"api/core/memory/#data-proxy","title":"Data Proxy","text":"<pre><code>memory = RobotLab.create_memory(\n data: { user: { name: \"Alice\", plan: \"pro\" } }\n)\n\nmemory.data[:user][:name] # =&gt; \"Alice\"\nmemory.data.to_h # =&gt; { user: { name: \"Alice\", plan: \"pro\" } }\n</code></pre>"},{"location":"api/core/memory/#serialization_1","title":"Serialization","text":"<pre><code># Save memory\njson = memory.to_json\nFile.write(\"memory.json\", json)\n\n# Restore memory\ndata = JSON.parse(File.read(\"memory.json\"))\nmemory = Memory.from_hash(data)\n</code></pre>"},{"location":"api/core/memory/#see-also","title":"See Also","text":"<ul> <li>Memory Guide</li> <li>State Management Architecture</li> </ul>"},{"location":"api/core/network/","title":"Network","text":"<p>Orchestrates multiple robots using SimpleFlow pipelines with DAG-based execution.</p>"},{"location":"api/core/network/#class-robotlabnetwork","title":"Class: <code>RobotLab::Network</code>","text":"<pre><code>network = RobotLab.create_network(name: \"support\", config: config) do\n task :classifier, classifier_robot, depends_on: :none\n task :billing, billing_robot, depends_on: :optional\nend\n</code></pre>"},{"location":"api/core/network/#attributes","title":"Attributes","text":""},{"location":"api/core/network/#name","title":"name","text":"<pre><code>network.name # =&gt; String\n</code></pre> <p>Network identifier for logging and debugging.</p>"},{"location":"api/core/network/#robots","title":"robots","text":"<pre><code>network.robots # =&gt; Hash&lt;String, Robot&gt;\n</code></pre> <p>Hash of robots keyed by name.</p>"},{"location":"api/core/network/#config","title":"config","text":"<pre><code>network.config # =&gt; RunConfig\n</code></pre> <p>Shared operational defaults for all robots in the network. Passed to robots during <code>run()</code> so they can inherit network-wide LLM settings. See RunConfig.</p>"},{"location":"api/core/network/#pipeline","title":"pipeline","text":"<pre><code>network.pipeline # =&gt; SimpleFlow::Pipeline\n</code></pre> <p>The underlying SimpleFlow pipeline.</p>"},{"location":"api/core/network/#methods","title":"Methods","text":""},{"location":"api/core/network/#run","title":"run","text":"<pre><code>result = network.run(\n message: \"Help me\",\n customer_id: 123,\n **context\n)\n# =&gt; SimpleFlow::Result\n</code></pre> <p>Execute the network pipeline.</p> <p>Parameters:</p> Name Type Description <code>message</code> <code>String</code> The input message <code>**context</code> <code>Hash</code> Additional context passed to all robots <p>Returns: <code>SimpleFlow::Result</code></p>"},{"location":"api/core/network/#task","title":"task","text":"<pre><code>network.task(name, robot, context: {}, mcp: :none, tools: :none, memory: nil, config: nil, depends_on: :none)\n# =&gt; self\n</code></pre> <p>Add a task to the pipeline with optional per-task configuration.</p> <p>Parameters:</p> Name Type Description <code>name</code> <code>Symbol</code> Task identifier <code>robot</code> <code>Robot</code> Robot instance to execute <code>context</code> <code>Hash</code> Task-specific context (deep-merged with run params) <code>mcp</code> <code>:none</code>, Array MCP server config (<code>:none</code> or array of servers) <code>tools</code> <code>:none</code>, Array Tools config (<code>:none</code> or array of tools) <code>memory</code> <code>Memory</code>, <code>nil</code> Task-specific memory <code>config</code> <code>RunConfig</code>, <code>nil</code> Per-task config (merged on top of network's RunConfig) <code>depends_on</code> <code>:none</code>, <code>Array&lt;Symbol&gt;</code>, <code>:optional</code> Task dependencies <p>Dependency Types:</p> Value Description <code>:none</code> No dependencies, runs first <code>[:task1, :task2]</code> Waits for listed tasks to complete <code>:optional</code> Only runs when explicitly activated"},{"location":"api/core/network/#add_robot","title":"add_robot","text":"<pre><code>network.add_robot(robot)\n# =&gt; self\n</code></pre> <p>Add a robot without creating a pipeline task. Useful for robots referenced by other tasks.</p>"},{"location":"api/core/network/#robot","title":"robot / []","text":"<pre><code>network.robot(\"billing\") # =&gt; Robot\nnetwork[\"billing\"] # =&gt; Robot (alias)\n</code></pre> <p>Get robot by name.</p>"},{"location":"api/core/network/#available_robots","title":"available_robots","text":"<pre><code>network.available_robots # =&gt; Array&lt;Robot&gt;\n</code></pre> <p>Returns all robot instances.</p>"},{"location":"api/core/network/#visualize","title":"visualize","text":"<pre><code>network.visualize # =&gt; String\n</code></pre> <p>ASCII visualization of the pipeline.</p>"},{"location":"api/core/network/#to_mermaid","title":"to_mermaid","text":"<pre><code>network.to_mermaid # =&gt; String\n</code></pre> <p>Mermaid diagram definition.</p>"},{"location":"api/core/network/#execution_plan","title":"execution_plan","text":"<pre><code>network.execution_plan # =&gt; String\n</code></pre> <p>Human-readable execution plan.</p>"},{"location":"api/core/network/#to_h","title":"to_h","text":"<pre><code>network.to_h # =&gt; Hash\n</code></pre> <p>Hash representation of network configuration.</p> <pre><code>{\n name: \"support\",\n robots: [\"classifier\", \"billing\", \"technical\"],\n tasks: [\"classifier\", \"billing\", \"technical\"],\n optional_tasks: [:billing, :technical],\n config: { model: \"claude-sonnet-4\", temperature: 0.7 } # if set\n}\n</code></pre>"},{"location":"api/core/network/#simpleflowresult","title":"SimpleFlow::Result","text":"<p>When <code>run</code> is called, a <code>SimpleFlow::Result</code> is returned:</p>"},{"location":"api/core/network/#attributes_1","title":"Attributes","text":"<pre><code>result.value # Final task's output (RobotResult)\nresult.context # Hash of all task results\nresult.halted? # Whether execution stopped early\nresult.continued? # Whether execution continues\n</code></pre>"},{"location":"api/core/network/#context-structure","title":"Context Structure","text":"<pre><code>result.context[:run_params] # Original run parameters\nresult.context[:classifier] # Classifier robot's RobotResult\nresult.context[:billing] # Billing robot's RobotResult (if activated)\n</code></pre>"},{"location":"api/core/network/#builder-dsl","title":"Builder DSL","text":""},{"location":"api/core/network/#task_1","title":"task","text":"<pre><code>network = RobotLab.create_network(name: \"pipeline\") do\n task :first, robot1, depends_on: :none\n task :second, robot2, depends_on: [:first]\n task :optional, robot3, depends_on: :optional\nend\n</code></pre>"},{"location":"api/core/network/#task-with-context","title":"task with context","text":"<pre><code>network = RobotLab.create_network(name: \"support\") do\n task :classifier, classifier_robot, depends_on: :none\n task :billing, billing_robot,\n context: { department: \"billing\", escalation_level: 2 },\n depends_on: :optional\n task :technical, technical_robot,\n context: { department: \"technical\" },\n tools: [DebugTool, LogTool],\n depends_on: :optional\nend\n</code></pre>"},{"location":"api/core/network/#examples","title":"Examples","text":""},{"location":"api/core/network/#sequential-pipeline","title":"Sequential Pipeline","text":"<pre><code>network = RobotLab.create_network(name: \"pipeline\") do\n task :extract, extractor, depends_on: :none\n task :transform, transformer, depends_on: [:extract]\n task :load, loader, depends_on: [:transform]\nend\n\nresult = network.run(message: \"Process this document\")\nputs result.value.last_text_content\n</code></pre>"},{"location":"api/core/network/#parallel-execution","title":"Parallel Execution","text":"<pre><code>network = RobotLab.create_network(name: \"analysis\", concurrency: :threads) do\n task :fetch, fetcher, depends_on: :none\n\n # Run in parallel\n task :sentiment, sentiment_bot, depends_on: [:fetch]\n task :entities, entity_bot, depends_on: [:fetch]\n\n # Wait for both\n task :merge, merger, depends_on: [:sentiment, :entities]\nend\n</code></pre>"},{"location":"api/core/network/#conditional-routing","title":"Conditional Routing","text":"<pre><code>class ClassifierRobot &lt; RobotLab::Robot\n def call(result)\n robot_result = run(**extract_run_context(result))\n\n new_result = result\n .with_context(@name.to_sym, robot_result)\n .continue(robot_result)\n\n category = robot_result.last_text_content.to_s.downcase\n case category\n when /billing/ then new_result.activate(:billing)\n when /technical/ then new_result.activate(:technical)\n else new_result.activate(:general)\n end\n end\nend\n\nnetwork = RobotLab.create_network(name: \"support\") do\n task :classifier, ClassifierRobot.new(name: \"classifier\", template: :classifier),\n depends_on: :none\n task :billing, billing_robot, depends_on: :optional\n task :technical, technical_robot, depends_on: :optional\n task :general, general_robot, depends_on: :optional\nend\n\nresult = network.run(message: \"I have a billing question\")\nputs result.value.last_text_content\n</code></pre>"},{"location":"api/core/network/#accessing-task-results","title":"Accessing Task Results","text":"<pre><code>result = network.run(message: \"Hello\")\n\n# Access individual task results\nclassifier_result = result.context[:classifier]\nputs \"Classification: #{classifier_result.last_text_content}\"\n\n# Check which optional task ran\nif result.context[:billing]\n puts \"Billing handled the request\"\nelsif result.context[:technical]\n puts \"Technical handled the request\"\nend\n</code></pre>"},{"location":"api/core/network/#see-also","title":"See Also","text":"<ul> <li>Creating Networks Guide</li> <li>Network Orchestration</li> <li>Robot</li> </ul>"},{"location":"api/core/robot/","title":"Robot","text":"<p>LLM-powered agent with template-based prompts, tools, memory, and MCP integration.</p>"},{"location":"api/core/robot/#class-hierarchy","title":"Class Hierarchy","text":"<pre><code>RubyLLM::Agent\n \u2514\u2500\u2500 RobotLab::Robot\n \u2514\u2500\u2500 Your custom subclasses (e.g., ClassifierRobot)\n</code></pre> <p><code>Robot</code> inherits from <code>RubyLLM::Agent</code>, which creates a persistent <code>@chat</code> on initialization. The robot adds template-based prompts, shared memory, hierarchical MCP configuration, and SimpleFlow pipeline integration on top of the base agent.</p>"},{"location":"api/core/robot/#constructor","title":"Constructor","text":"<pre><code>Robot.new(\n name:,\n template: nil,\n system_prompt: nil,\n context: {},\n description: nil,\n local_tools: [],\n model: nil,\n mcp_servers: [],\n mcp: :none,\n tools: :none,\n on_tool_call: nil,\n on_tool_result: nil,\n on_content: nil,\n enable_cache: true,\n bus: nil,\n skills: nil,\n temperature: nil,\n top_p: nil,\n top_k: nil,\n max_tokens: nil,\n presence_penalty: nil,\n frequency_penalty: nil,\n stop: nil,\n config: nil\n)\n</code></pre>"},{"location":"api/core/robot/#parameters","title":"Parameters","text":"Name Type Default Description <code>name</code> <code>String</code> required Unique identifier for the robot <code>template</code> <code>Symbol</code>, <code>nil</code> <code>nil</code> Prompt template (e.g., <code>:assistant</code> loads <code>prompts/assistant.md</code>) <code>system_prompt</code> <code>String</code>, <code>nil</code> <code>nil</code> Inline system prompt (appended after template if both given) <code>context</code> <code>Hash</code>, <code>Proc</code> <code>{}</code> Variables passed to the template <code>description</code> <code>String</code>, <code>nil</code> <code>nil</code> Human-readable description of what the robot does <code>local_tools</code> <code>Array</code> <code>[]</code> Tools defined locally (<code>RubyLLM::Tool</code> subclasses or <code>RobotLab::Tool</code> instances) <code>model</code> <code>String</code>, <code>nil</code> <code>nil</code> LLM model ID (falls back to <code>RobotLab.config.ruby_llm.model</code>) <code>mcp_servers</code> <code>Array</code> <code>[]</code> Legacy MCP server configurations <code>mcp</code> <code>Symbol</code>, <code>Array</code> <code>:none</code> Hierarchical MCP config (<code>:none</code>, <code>:inherit</code>, or server array) <code>tools</code> <code>Symbol</code>, <code>Array</code> <code>:none</code> Hierarchical tools config (<code>:none</code>, <code>:inherit</code>, or tool name array) <code>on_tool_call</code> <code>Proc</code>, <code>nil</code> <code>nil</code> Callback invoked when a tool is called <code>on_tool_result</code> <code>Proc</code>, <code>nil</code> <code>nil</code> Callback invoked when a tool returns a result <code>on_content</code> <code>Proc</code>, <code>nil</code> <code>nil</code> Stored streaming callback invoked with each content chunk (see Streaming) <code>enable_cache</code> <code>Boolean</code> <code>true</code> Whether to enable semantic caching <code>bus</code> <code>TypedBus::MessageBus</code>, <code>nil</code> <code>nil</code> Optional message bus for inter-robot communication <code>skills</code> <code>Symbol</code>, <code>Array&lt;Symbol&gt;</code>, <code>nil</code> <code>nil</code> Skill templates to prepend (see Skills) <code>config</code> <code>RunConfig</code>, <code>nil</code> <code>nil</code> Shared config merged with explicit kwargs (see RunConfig) <code>temperature</code> <code>Float</code>, <code>nil</code> <code>nil</code> Controls randomness (0.0-1.0) <code>top_p</code> <code>Float</code>, <code>nil</code> <code>nil</code> Nucleus sampling threshold <code>top_k</code> <code>Integer</code>, <code>nil</code> <code>nil</code> Top-k sampling <code>max_tokens</code> <code>Integer</code>, <code>nil</code> <code>nil</code> Maximum tokens in response <code>presence_penalty</code> <code>Float</code>, <code>nil</code> <code>nil</code> Penalize based on presence <code>frequency_penalty</code> <code>Float</code>, <code>nil</code> <code>nil</code> Penalize based on frequency <code>stop</code> <code>String</code>, <code>Array</code>, <code>nil</code> <code>nil</code> Stop sequences <p>When both <code>config:</code> and explicit kwargs (e.g., <code>temperature:</code>) are provided, explicit kwargs always win.</p>"},{"location":"api/core/robot/#factory-method","title":"Factory Method","text":"<pre><code>robot = RobotLab.build(\n name: \"robot\", # Defaults to \"robot\"\n template: nil,\n system_prompt: nil,\n context: {},\n enable_cache: true,\n bus: nil, # Optional TypedBus::MessageBus\n skills: nil, # Optional skill templates\n **options # All other Robot.new parameters\n)\n# =&gt; RobotLab::Robot\n</code></pre> <p>If <code>name</code> is omitted, it defaults to <code>\"robot\"</code>.</p>"},{"location":"api/core/robot/#attributes-read-only","title":"Attributes (Read-Only)","text":"Attribute Type Description <code>name</code> <code>String</code> Unique identifier <code>description</code> <code>String</code>, <code>nil</code> Human-readable description <code>template</code> <code>Symbol</code>, <code>nil</code> Prompt template identifier <code>system_prompt</code> <code>String</code>, <code>nil</code> Inline system prompt <code>skills</code> <code>Array&lt;Symbol&gt;</code>, <code>nil</code> Constructor-provided skill template IDs (nil if none) <code>local_tools</code> <code>Array</code> Locally defined tools <code>mcp_clients</code> <code>Hash&lt;String, MCP::Client&gt;</code> Connected MCP clients, keyed by server name <code>mcp_tools</code> <code>Array&lt;Tool&gt;</code> Tools discovered from MCP servers <code>memory</code> <code>Memory</code> Inherent memory (used when standalone, not in network) <code>bus</code> <code>TypedBus::MessageBus</code>, <code>nil</code> Message bus instance (nil if not configured) <code>outbox</code> <code>Hash</code> Sent messages tracked by composite key with status and replies <code>config</code> <code>RunConfig</code> Effective RunConfig (merged from constructor kwargs and passed-in config) <code>mcp_config</code> <code>Symbol</code>, <code>Array</code> Build-time MCP configuration (raw, unresolved) <code>tools_config</code> <code>Symbol</code>, <code>Array</code> Build-time tools configuration (raw, unresolved)"},{"location":"api/core/robot/#attributes-read-write","title":"Attributes (Read-Write)","text":"Attribute Type Default Description <code>input</code> <code>IO</code>, <code>nil</code> <code>nil</code> Input stream for user interaction (falls back to <code>$stdin</code>) <code>output</code> <code>IO</code>, <code>nil</code> <code>nil</code> Output stream for user interaction (falls back to <code>$stdout</code>) <p>Used by tools like <code>AskUser</code> that need terminal IO. Set to <code>StringIO</code> for testing.</p>"},{"location":"api/core/robot/#methods","title":"Methods","text":""},{"location":"api/core/robot/#run","title":"run","text":"<pre><code>result = robot.run(message, **kwargs, &amp;block)\n# =&gt; RobotResult\n</code></pre> <p>Primary execution method. Sends a message to the LLM with memory/MCP/tools resolution and returns a <code>RobotResult</code>.</p> <p>Parameters:</p> Name Type Default Description <code>message</code> <code>String</code> required The user message to send <code>network</code> <code>NetworkRun</code>, <code>nil</code> <code>nil</code> Network context (passed internally) <code>network_memory</code> <code>Memory</code>, <code>nil</code> <code>nil</code> Shared network memory <code>memory</code> <code>Memory</code>, <code>Hash</code>, <code>nil</code> <code>nil</code> Runtime memory to merge <code>mcp</code> <code>Symbol</code>, <code>Array</code> <code>:none</code> Runtime MCP override <code>tools</code> <code>Symbol</code>, <code>Array</code> <code>:none</code> Runtime tools override <code>**kwargs</code> <code>Hash</code> <code>{}</code> Additional keyword arguments passed to <code>Agent#ask</code> <code>&amp;block</code> <code>Proc</code> <code>nil</code> Per-call streaming block, receives each content chunk <p>When both a stored <code>on_content</code> callback and a runtime block are provided, both fire (stored first, then runtime block).</p> <p>Returns: <code>RobotResult</code></p> <p>Examples:</p> <pre><code># Simple message\nresult = robot.run(\"What is 2+2?\")\n\n# With runtime memory\nresult = robot.run(\"Summarize the data\", memory: { data: report })\n\n# With per-call streaming block\nresult = robot.run(\"Tell me a story\") { |chunk| print chunk.content }\n\n# With runtime overrides\nresult = robot.run(\"Help me\", mcp: :none, tools: :none)\n</code></pre>"},{"location":"api/core/robot/#model","title":"model","text":"<pre><code>robot.model # =&gt; \"claude-sonnet-4\" or nil\n</code></pre> <p>Returns the model ID string. Resolves through the underlying chat object.</p>"},{"location":"api/core/robot/#update","title":"update","text":"<pre><code>robot.update(\n template: nil,\n context: nil,\n system_prompt: nil,\n model: nil,\n temperature: nil,\n **kwargs\n)\n# =&gt; self\n</code></pre> <p>Reconfigure the robot after construction. Returns <code>self</code> for chaining.</p>"},{"location":"api/core/robot/#with_-methods-chaining","title":"with_* Methods (Chaining)","text":"<p>All <code>with_*</code> methods delegate to the persistent <code>@chat</code> and return <code>self</code> for chaining:</p> Method Description <code>with_model(model_id)</code> Change the LLM model <code>with_temperature(temp)</code> Set temperature <code>with_top_p(value)</code> Set nucleus sampling <code>with_top_k(value)</code> Set top-k sampling <code>with_max_tokens(value)</code> Set max response tokens <code>with_presence_penalty(value)</code> Set presence penalty <code>with_frequency_penalty(value)</code> Set frequency penalty <code>with_stop(sequences)</code> Set stop sequences <code>with_instructions(prompt)</code> Set system instructions <code>with_tool(tool)</code> Add a single tool <code>with_tools(*tools)</code> Add multiple tools <code>with_params(**params)</code> Set additional parameters <code>with_headers(**headers)</code> Set custom headers <code>with_schema(schema)</code> Set output schema <code>with_context(**ctx)</code> Set context <code>with_thinking(opts)</code> Enable extended thinking <code>with_bus(bus)</code> Connect to a message bus (creates one if nil) <p>Example:</p> <pre><code>robot = RobotLab.build(name: \"bot\")\nrobot\n .with_model(\"claude-sonnet-4\")\n .with_temperature(0.7)\n .with_instructions(\"Be concise.\")\n .run(\"Hello\")\n</code></pre>"},{"location":"api/core/robot/#with_template","title":"with_template","text":"<pre><code>robot.with_template(:assistant, tone: \"friendly\")\n# =&gt; self\n</code></pre> <p>Apply a prompt_manager template. Separate from the delegated <code>with_*</code> methods because it handles template parsing and front matter config.</p>"},{"location":"api/core/robot/#call","title":"call","text":"<pre><code>robot.call(result)\n# =&gt; SimpleFlow::Result\n</code></pre> <p>SimpleFlow step interface. Extracts the message from <code>result.context[:run_params]</code>, calls <code>run</code>, and wraps the output in a continued <code>SimpleFlow::Result</code>.</p> <p>Override this method in subclasses for custom routing logic (e.g., classifiers).</p>"},{"location":"api/core/robot/#reset_memory","title":"reset_memory","text":"<pre><code>robot.reset_memory\n# =&gt; self\n</code></pre> <p>Reset the robot's inherent memory to its initial state.</p>"},{"location":"api/core/robot/#send_message","title":"send_message","text":"<pre><code>message = robot.send_message(to: :bob, content: \"Tell me a joke.\")\n# =&gt; RobotMessage\n</code></pre> <p>Publish a message to another robot's bus channel. Increments the internal message counter, creates a <code>RobotMessage</code>, tracks it in the outbox, and publishes to the target channel.</p> <p>Parameters:</p> Name Type Description <code>to</code> <code>String</code>, <code>Symbol</code> Target robot's channel name <code>content</code> <code>String</code>, <code>Hash</code> Message payload <p>Returns: <code>RobotMessage</code></p> <p>Raises: <code>BusError</code> if no bus is configured.</p>"},{"location":"api/core/robot/#send_reply","title":"send_reply","text":"<pre><code>reply = robot.send_reply(to: :alice, content: \"Here's a joke...\", in_reply_to: \"alice:1\")\n# =&gt; RobotMessage\n</code></pre> <p>Publish a correlated reply to a specific message. The <code>in_reply_to</code> composite key links this reply to the original message.</p> <p>Parameters:</p> Name Type Description <code>to</code> <code>String</code>, <code>Symbol</code> Target robot's channel name <code>content</code> <code>String</code>, <code>Hash</code> Reply payload <code>in_reply_to</code> <code>String</code> Composite key of the original message (e.g., <code>\"alice:1\"</code>) <p>Returns: <code>RobotMessage</code></p> <p>Raises: <code>BusError</code> if no bus is configured.</p>"},{"location":"api/core/robot/#on_message","title":"on_message","text":"<pre><code>robot.on_message { |message| puts message.content }\n# =&gt; self\n</code></pre> <p>Register a custom handler for incoming bus messages. Block arity controls delivery handling:</p> <ul> <li>1 argument <code>|message|</code> \u2014 auto-acknowledges the delivery before calling the block</li> <li>2 arguments <code>|delivery, message|</code> \u2014 manual mode; you call <code>delivery.ack!</code> or <code>delivery.nack!</code></li> </ul> <p>Examples:</p> <pre><code># Auto-ack mode (1 arg)\nrobot.on_message do |message|\n joke = run(message.content.to_s).last_text_content\n send_reply(to: message.from.to_sym, content: joke, in_reply_to: message.key)\nend\n\n# Manual mode (2 args)\nrobot.on_message do |delivery, message|\n if message.content.to_s.length &gt; 10\n delivery.ack!\n send_reply(to: message.from.to_sym, content: \"Got it!\", in_reply_to: message.key)\n else\n delivery.nack!\n end\nend\n</code></pre>"},{"location":"api/core/robot/#spawn","title":"spawn","text":"<pre><code>child = robot.spawn(\n name: \"specialist\",\n system_prompt: \"You are a specialist.\"\n)\n# =&gt; RobotLab::Robot (connected to same bus)\n</code></pre> <p>Create a new robot on the same message bus. If the parent has no bus, one is created automatically and the parent is connected to it.</p> <p>Parameters:</p> Name Type Default Description <code>name</code> <code>String</code> <code>\"robot\"</code> Name for the new robot <code>system_prompt</code> <code>String</code>, <code>nil</code> <code>nil</code> Inline system prompt <code>template</code> <code>Symbol</code>, <code>nil</code> <code>nil</code> Prompt template <code>local_tools</code> <code>Array</code> <code>[]</code> Tools for the new robot <code>**options</code> <code>Hash</code> <code>{}</code> Additional options passed to <code>RobotLab.build</code> <p>Returns: <code>Robot</code></p> <p>Examples:</p> <pre><code># Minimal spawn (bus created automatically)\nbot = RobotLab.build\nbot2 = bot.spawn(system_prompt: \"You are helpful.\")\n\n# Spawn with template\nspecialist = dispatcher.spawn(\n name: \"billing\",\n template: :billing,\n local_tools: [InvoiceLookup]\n)\n\n# Fan-out: multiple robots with the same name\nworker1 = bot.spawn(name: \"worker\", system_prompt: \"Worker 1\")\nworker2 = bot.spawn(name: \"worker\", system_prompt: \"Worker 2\")\n# Messages sent to :worker are delivered to both\n</code></pre>"},{"location":"api/core/robot/#with_bus","title":"with_bus","text":"<pre><code>robot.with_bus(bus)\n# =&gt; self\n</code></pre> <p>Connect the robot to a message bus after creation. If called without an argument and the robot has no bus, a new one is created. Returns <code>self</code> for chaining.</p> <p>Parameters:</p> Name Type Default Description <code>bus</code> <code>TypedBus::MessageBus</code>, <code>nil</code> <code>nil</code> Bus to join (creates one if nil and robot has no bus) <p>Returns: <code>self</code></p> <p>Examples:</p> <pre><code># Join an existing bus\nbot = RobotLab.build(name: \"bot\")\nbot.with_bus(some_bus)\n\n# Create a bus on demand\nbot = RobotLab.build(name: \"bot\").with_bus\n\n# Switch buses\nbot.with_bus(bus1) # joins bus1\nbot.with_bus(bus2) # leaves bus1, joins bus2\n</code></pre>"},{"location":"api/core/robot/#disconnect","title":"disconnect","text":"<pre><code>robot.disconnect\n# =&gt; self\n</code></pre> <p>Disconnect from all MCP servers and bus channels.</p>"},{"location":"api/core/robot/#to_h","title":"to_h","text":"<pre><code>robot.to_h\n# =&gt; Hash\n</code></pre> <p>Returns a hash representation of the robot including name, description, template, skills, system_prompt, local_tools, mcp_tools, mcp_config, tools_config, mcp_servers, model, and bus (true if configured, omitted otherwise). Nil values are compacted out.</p>"},{"location":"api/core/robot/#memory-behavior","title":"Memory Behavior","text":"<ul> <li>Standalone: Robot uses its own inherent <code>Memory</code> instance (<code>robot.memory</code>).</li> <li>In a Network: Robot uses the network's shared memory (passed via <code>network_memory:</code>).</li> </ul> <pre><code># Standalone memory access\nrobot.memory[:user_id] = 123\nrobot.memory[:user_id] # =&gt; 123\n\n# Reset standalone memory\nrobot.reset_memory\n</code></pre>"},{"location":"api/core/robot/#templates","title":"Templates","text":"<p>Templates are <code>.md</code> files with optional YAML front matter, loaded via <code>prompt_manager</code>. The <code>template:</code> parameter maps to a file path relative to the configured template directory:</p> <pre><code># template: :assistant =&gt; prompts/assistant.md\nrobot = RobotLab.build(name: \"bot\", template: :assistant, context: { tone: \"friendly\" })\n</code></pre> <p>Front matter supports two categories of keys:</p> <p>LLM Config: <code>model</code>, <code>temperature</code>, <code>top_p</code>, <code>top_k</code>, <code>max_tokens</code>, <code>presence_penalty</code>, <code>frequency_penalty</code>, <code>stop</code> \u2014 applied to the underlying chat.</p> <p>Robot Extras: <code>robot_name</code>, <code>description</code>, <code>tools</code>, <code>mcp</code>, <code>skills</code> \u2014 applied to the robot's identity and capabilities. Constructor-provided values always take precedence.</p> Key Type Description <code>robot_name</code> <code>String</code> Override robot name (when constructor uses the default <code>\"robot\"</code>) <code>description</code> <code>String</code> Human-readable description <code>tools</code> <code>Array&lt;String&gt;</code> Tool class names resolved via <code>Object.const_get</code> <code>mcp</code> <code>Array&lt;Hash&gt;</code> MCP server configurations <code>skills</code> <code>Array&lt;Symbol&gt;</code> Skill templates to prepend (recursive, with cycle detection)"},{"location":"api/core/robot/#skills","title":"Skills","text":"<p>Skills compose robot behaviors from reusable templates. Each skill is a standard <code>.md</code> template whose prompt body is prepended before the main template. Skills are expanded depth-first with automatic cycle detection.</p> <p>Constructor: <code>skills:</code> accepts <code>Symbol</code> or <code>Array&lt;Symbol&gt;</code>:</p> <pre><code>robot = RobotLab.build(\n name: \"support\",\n template: :support,\n skills: [:clarifier, :json_responder]\n)\n</code></pre> <p>Front matter: templates can declare skills via <code>skills:</code> key:</p> <pre><code>---\nskills:\n - clarifier\n - json_responder\n---\nMain template body here.\n</code></pre> <p>Constructor <code>skills:</code> and front matter <code>skills:</code> are combined (constructor first, then front matter). Skills can nest (a skill can declare its own <code>skills:</code> in front matter).</p> <p>Config cascade: skill config merges in processing order (deepest first). Later values override earlier. Constructor kwargs always win.</p> <p>Prompt order: skill bodies are concatenated in expansion order, followed by the main template body. All are joined with <code>\"\\n\\n\"</code> and set as system instructions via a single <code>with_instructions</code> call.</p> <p>Cycle detection: if skills form a cycle, the duplicate is skipped with a logger warning.</p>"},{"location":"api/core/robot/#runconfig","title":"RunConfig","text":"<p><code>RunConfig</code> provides shared operational defaults that flow through the configuration hierarchy. Pass it via the <code>config:</code> parameter on <code>Robot.new</code> or <code>RobotLab.build</code>.</p> <pre><code>shared = RobotLab::RunConfig.new(model: \"claude-sonnet-4\", temperature: 0.7)\n\nrobot = RobotLab.build(\n name: \"writer\",\n system_prompt: \"You write creatively.\",\n config: shared,\n temperature: 0.9 # explicit kwargs override config\n)\n\nrobot.config #=&gt; RunConfig with model: \"claude-sonnet-4\", temperature: 0.9, ...\n</code></pre> <p>RunConfig fields: <code>model</code>, <code>temperature</code>, <code>top_p</code>, <code>top_k</code>, <code>max_tokens</code>, <code>presence_penalty</code>, <code>frequency_penalty</code>, <code>stop</code>, <code>mcp</code>, <code>tools</code>, <code>on_tool_call</code>, <code>on_tool_result</code>, <code>on_content</code>, <code>bus</code>, <code>enable_cache</code>.</p> <p>See Configuration: RunConfig for full details.</p>"},{"location":"api/core/robot/#streaming","title":"Streaming","text":"<p>Robots support two complementary approaches for streaming LLM content in real-time.</p>"},{"location":"api/core/robot/#the-chunk-object","title":"The Chunk Object","text":"<p>Both callbacks and blocks receive a <code>RubyLLM::Chunk</code> (subclass of <code>RubyLLM::Message</code>). Key accessors:</p> Accessor Type Description <code>content</code> <code>String</code>, <code>nil</code> The text delta for this chunk (<code>nil</code> on tool-call or usage-only chunks) <code>role</code> <code>Symbol</code> Always <code>:assistant</code> <code>model_id</code> <code>String</code> The LLM model ID <code>tool_calls</code> <code>Array</code>, <code>nil</code> Tool call deltas (partial JSON arguments) <code>tool_call?</code> <code>Boolean</code> Whether this chunk contains tool call data <code>thinking</code> <code>Thinking</code>, <code>nil</code> Extended thinking delta (Anthropic only) <code>input_tokens</code> <code>Integer</code>, <code>nil</code> Input token count (populated on final chunk) <code>output_tokens</code> <code>Integer</code>, <code>nil</code> Output token count (populated on final chunk) <code>cached_tokens</code> <code>Integer</code>, <code>nil</code> Cached prompt tokens (final chunk) <p>Most chunks carry only <code>content</code> (the text delta). The final chunk(s) carry token usage counts. Tool call chunks have <code>tool_calls</code> instead of <code>content</code>.</p>"},{"location":"api/core/robot/#stored-callback-on_content","title":"Stored Callback (<code>on_content:</code>)","text":"<p>Wired at build time via constructor or RunConfig. Fires on every <code>run()</code> call automatically:</p> <pre><code>robot = RobotLab.build(\n name: \"assistant\",\n system_prompt: \"You are helpful.\",\n on_content: -&gt;(chunk) { broadcast(chunk.content) }\n)\nrobot.run(\"Tell me a story\") # streams via stored callback\n</code></pre> <p>The <code>on_content</code> callback participates in the RunConfig cascade:</p> <pre><code>config = RobotLab::RunConfig.new(\n on_content: -&gt;(chunk) { log(chunk.content) }\n)\nrobot = RobotLab.build(name: \"bot\", config: config)\n</code></pre> <p>Constructor <code>on_content:</code> overrides RunConfig <code>on_content</code>.</p>"},{"location":"api/core/robot/#per-call-block","title":"Per-Call Block","text":"<p>Pass a block to <code>run()</code> for one-off streaming:</p> <pre><code>robot.run(\"Tell me a story\") { |chunk| print chunk.content }\n</code></pre>"},{"location":"api/core/robot/#both-together","title":"Both Together","text":"<p>When both exist, both fire \u2014 stored callback first, then runtime block:</p> <pre><code>robot = RobotLab.build(\n name: \"bot\",\n system_prompt: \"You are helpful.\",\n on_content: -&gt;(chunk) { log(chunk.content) }\n)\nrobot.run(\"Tell me a story\") { |chunk| stream_to_client(chunk.content) }\n# log() fires first, then stream_to_client()\n</code></pre>"},{"location":"api/core/robot/#configuration-hierarchy","title":"Configuration Hierarchy","text":"<p>Tools and MCP servers use hierarchical resolution: runtime &gt; robot &gt; network &gt; global config.</p> <pre><code>RobotLab.config (global)\n |\n +-- Network (config:)\n | |\n | +-- Task (config:)\n | | |\n | | +-- Robot (config: + build-time mcp:, tools:)\n | | |\n | | +-- Template front matter\n | | |\n | | +-- run() call (runtime mcp:, tools:)\n</code></pre> <p>Values at each level:</p> <ul> <li><code>:none</code> -- no tools/MCP at this level</li> <li><code>:inherit</code> -- inherit from parent level</li> <li><code>Array</code> -- explicit list of tool names or MCP server configs</li> </ul>"},{"location":"api/core/robot/#examples","title":"Examples","text":""},{"location":"api/core/robot/#basic-robot","title":"Basic Robot","text":"<pre><code>robot = RobotLab.build(\n name: \"greeter\",\n system_prompt: \"You greet users warmly.\"\n)\nresult = robot.run(\"Hello!\")\nputs result.last_text_content\n</code></pre>"},{"location":"api/core/robot/#robot-with-template","title":"Robot with Template","text":"<pre><code>robot = RobotLab.build(\n name: \"support\",\n template: :support,\n context: { company: \"Acme Corp\" }\n)\nresult = robot.run(\"I need help with my order\")\n</code></pre>"},{"location":"api/core/robot/#robot-with-tools","title":"Robot with Tools","text":"<pre><code>class Calculator &lt; RubyLLM::Tool\n description \"Performs basic arithmetic\"\n param :operation, type: \"string\", desc: \"add, subtract, multiply, divide\"\n param :a, type: \"number\", desc: \"First operand\"\n param :b, type: \"number\", desc: \"Second operand\"\n\n def execute(operation:, a:, b:)\n case operation\n when \"add\" then a + b\n when \"subtract\" then a - b\n when \"multiply\" then a * b\n when \"divide\" then a.to_f / b\n end\n end\nend\n\nrobot = RobotLab.build(\n name: \"math_bot\",\n system_prompt: \"You help with math.\",\n local_tools: [Calculator]\n)\nresult = robot.run(\"What is 15 * 7?\")\n</code></pre>"},{"location":"api/core/robot/#robot-with-mcp","title":"Robot with MCP","text":"<pre><code>robot = RobotLab.build(\n name: \"developer\",\n system_prompt: \"You help with coding tasks.\",\n mcp: [\n {\n name: \"github\",\n transport: { type: \"stdio\", command: \"github-mcp-server\", args: [\"stdio\"] }\n }\n ]\n)\nresult = robot.run(\"Search for popular Ruby repos\")\nrobot.disconnect\n</code></pre>"},{"location":"api/core/robot/#robot-with-skills","title":"Robot with Skills","text":"<pre><code>robot = RobotLab.build(\n name: \"support\",\n template: :support,\n skills: [:clarifier, :safety, :json_responder],\n context: { company: \"Acme Corp\" }\n)\nresult = robot.run(\"I need help with my order\")\n</code></pre>"},{"location":"api/core/robot/#bare-robot-with-chaining","title":"Bare Robot with Chaining","text":"<pre><code>robot = RobotLab.build(name: \"bot\")\nresult = robot\n .with_instructions(\"Be concise.\")\n .with_temperature(0.3)\n .run(\"Explain quantum computing\")\n</code></pre>"},{"location":"api/core/robot/#robot-with-message-bus","title":"Robot with Message Bus","text":"<pre><code>bus = TypedBus::MessageBus.new\n\nbob = RobotLab.build(name: \"bob\", system_prompt: \"You tell jokes.\", bus: bus)\n\nalice = RobotLab.build(name: \"alice\", system_prompt: \"You evaluate jokes.\", bus: bus)\nalice.on_message do |message|\n verdict = alice.run(\"Is this funny? #{message.content}\").last_text_content\n puts verdict\nend\n\nbob.on_message do |message|\n joke = bob.run(message.content.to_s).last_text_content\n bob.send_reply(to: message.from.to_sym, content: joke, in_reply_to: message.key)\nend\n\nalice.send_message(to: :bob, content: \"Tell me a robot joke.\")\n</code></pre>"},{"location":"api/core/robot/#spawning-robots-dynamically","title":"Spawning Robots Dynamically","text":"<pre><code># Parent robot spawns specialists on demand\ndispatcher = RobotLab.build(\n name: \"dispatcher\",\n system_prompt: \"You delegate work.\"\n)\n\ndispatcher.on_message do |message|\n puts \"Reply from #{message.from}: #{message.content}\"\nend\n\n# spawn creates child on same bus (bus created lazily)\nhelper = dispatcher.spawn(\n name: \"helper\",\n system_prompt: \"You answer questions concisely.\"\n)\n\nanswer = helper.run(\"What is 2+2?\").last_text_content\nhelper.send_message(to: :dispatcher, content: answer)\n</code></pre>"},{"location":"api/core/robot/#connecting-to-a-bus-after-creation","title":"Connecting to a Bus After Creation","text":"<pre><code>bot = RobotLab.build(name: \"latecomer\", system_prompt: \"Hi there.\")\n\n# Join a bus later\nbus = TypedBus::MessageBus.new\nbot.with_bus(bus)\n\n# Now bot can send/receive messages\nbot.send_message(to: :someone, content: \"Hello!\")\n</code></pre>"},{"location":"api/core/robot/#see-also","title":"See Also","text":"<ul> <li>Building Robots Guide (includes Composable Skills)</li> <li>Tool</li> <li>Network</li> </ul>"},{"location":"api/core/state/","title":"Memory (State Management)","text":"<p>Memory manages conversation data, results, and runtime state. There is no separate <code>State</code> class; <code>Memory</code> serves this role.</p>"},{"location":"api/core/state/#class-robotlabmemory","title":"Class: <code>RobotLab::Memory</code>","text":"<pre><code>memory = RobotLab.create_memory(\n data: { user_id: \"123\" }\n)\n</code></pre>"},{"location":"api/core/state/#attributes","title":"Attributes","text":""},{"location":"api/core/state/#session_id","title":"session_id","text":"<pre><code>memory.session_id # =&gt; String | nil\n</code></pre> <p>Conversation session identifier for persistence.</p>"},{"location":"api/core/state/#data","title":"data","text":"<pre><code>memory.data # =&gt; StateProxy\n</code></pre> <p>Access workflow data as a proxy object.</p> <pre><code>memory.data[:user_id] # Hash access\nmemory.data.user_id # Method access\nmemory.data[:status] = \"active\"\n</code></pre>"},{"location":"api/core/state/#results","title":"results","text":"<pre><code>memory.results # =&gt; Array&lt;RobotResult&gt;\n</code></pre> <p>All robot execution results.</p>"},{"location":"api/core/state/#messages","title":"messages","text":"<pre><code>memory.messages # =&gt; Array&lt;Message&gt;\n</code></pre> <p>Formatted conversation messages for LLM.</p>"},{"location":"api/core/state/#methods","title":"Methods","text":""},{"location":"api/core/state/#append_result","title":"append_result","text":"<pre><code>memory.append_result(robot_result)\n</code></pre> <p>Add a robot result to history.</p>"},{"location":"api/core/state/#set_results","title":"set_results","text":"<pre><code>memory.set_results(array_of_results)\n</code></pre> <p>Replace all results.</p>"},{"location":"api/core/state/#results_from","title":"results_from","text":"<pre><code>memory.results_from(5) # =&gt; Array&lt;RobotResult&gt;\n</code></pre> <p>Get results starting at index.</p>"},{"location":"api/core/state/#session_id_1","title":"session_id=","text":"<pre><code>memory.session_id = \"session_123\"\n</code></pre> <p>Set the session identifier.</p>"},{"location":"api/core/state/#format_history","title":"format_history","text":"<pre><code>memory.format_history # =&gt; Array&lt;Message&gt;\n</code></pre> <p>Format results as conversation history.</p>"},{"location":"api/core/state/#clone","title":"clone","text":"<pre><code>new_memory = memory.clone\n</code></pre> <p>Create a deep copy.</p>"},{"location":"api/core/state/#to_h","title":"to_h","text":"<pre><code>memory.to_h # =&gt; Hash\n</code></pre> <p>Hash representation.</p>"},{"location":"api/core/state/#to_json","title":"to_json","text":"<pre><code>memory.to_json # =&gt; String\n</code></pre> <p>JSON representation.</p>"},{"location":"api/core/state/#from_hash-class-method","title":"from_hash (class method)","text":"<pre><code>memory = Memory.from_hash(hash)\n</code></pre> <p>Restore from hash.</p>"},{"location":"api/core/state/#stateproxy","title":"StateProxy","text":"<p>The <code>data</code> attribute is a <code>StateProxy</code>:</p> <pre><code>proxy = memory.data\n\n# Hash-style access\nproxy[:key]\nproxy[:key] = value\n\n# Method-style access\nproxy.key\nproxy.key = value\n\n# Hash operations\nproxy.key?(:key)\nproxy.keys\nproxy.values\nproxy.each { |k, v| ... }\nproxy.merge!(other_hash)\nproxy.delete(:key)\nproxy.to_h\nproxy.empty?\nproxy.size\n</code></pre>"},{"location":"api/core/state/#creating-memory","title":"Creating Memory","text":""},{"location":"api/core/state/#basic","title":"Basic","text":"<pre><code>memory = RobotLab.create_memory\n</code></pre>"},{"location":"api/core/state/#with-data","title":"With Data","text":"<pre><code>memory = RobotLab.create_memory(\n data: {\n user_id: \"user_123\",\n order_id: \"ord_456\"\n }\n)\n</code></pre>"},{"location":"api/core/state/#with-session-id","title":"With Session ID","text":"<pre><code>memory = RobotLab.create_memory\nmemory.session_id = \"session_123\"\n</code></pre>"},{"location":"api/core/state/#with-existing-results","title":"With Existing Results","text":"<pre><code>memory = RobotLab.create_memory\nmemory.set_results(previous_results)\n</code></pre>"},{"location":"api/core/state/#usermessage","title":"UserMessage","text":"<p>Enhanced message with metadata:</p> <pre><code>message = UserMessage.new(\n \"What's my order status?\",\n session_id: \"session_123\",\n system_prompt: \"Respond in Spanish\",\n metadata: { source: \"web\" }\n)\n\nmessage.content # =&gt; \"What's my order status?\"\nmessage.session_id # =&gt; \"session_123\"\nmessage.system_prompt # =&gt; \"Respond in Spanish\"\nmessage.metadata # =&gt; { source: \"web\" }\nmessage.id # =&gt; UUID\nmessage.created_at # =&gt; Time\n</code></pre>"},{"location":"api/core/state/#examples","title":"Examples","text":""},{"location":"api/core/state/#accessing-data","title":"Accessing Data","text":"<pre><code>memory = RobotLab.create_memory(\n data: { user: { name: \"Alice\", plan: \"pro\" } }\n)\n\nmemory.data[:user][:name] # =&gt; \"Alice\"\nmemory.data.to_h # =&gt; { user: { name: \"Alice\", plan: \"pro\" } }\n</code></pre>"},{"location":"api/core/state/#working-with-results","title":"Working with Results","text":"<pre><code># After running network\nmemory.results.size # Number of results\nmemory.results.last # Most recent\nmemory.results.map(&amp;:robot_name) # [\"classifier\", \"support\"]\n</code></pre>"},{"location":"api/core/state/#using-reactive-memory","title":"Using Reactive Memory","text":"<pre><code>memory.set(:intent, \"billing\")\nintent = memory.get(:intent)\n\nmemory.subscribe(:status) do |change|\n puts \"Status changed to #{change.value} by #{change.writer}\"\nend\n</code></pre>"},{"location":"api/core/state/#serialization","title":"Serialization","text":"<pre><code># Save memory\njson = memory.to_json\nFile.write(\"memory.json\", json)\n\n# Restore memory\ndata = JSON.parse(File.read(\"memory.json\"))\nmemory = Memory.from_hash(data)\n</code></pre>"},{"location":"api/core/state/#see-also","title":"See Also","text":"<ul> <li>State Management Architecture</li> <li>Memory</li> </ul>"},{"location":"api/core/tool/","title":"Tool","text":"<p>Callable function that robots can use to interact with external systems.</p>"},{"location":"api/core/tool/#class-robotlabtool-rubyllmtool","title":"Class: <code>RobotLab::Tool &lt; RubyLLM::Tool</code>","text":"<p>RobotLab::Tool inherits from RubyLLM::Tool, adding a <code>robot:</code> constructor parameter, a <code>Tool.create</code> factory for dynamic tools, and graceful error handling that returns plain-text errors to the LLM instead of crashing the run.</p>"},{"location":"api/core/tool/#subclass-pattern","title":"Subclass Pattern","text":"<pre><code>class GetWeather &lt; RobotLab::Tool\n description \"Get weather for a location\"\n\n param :location, type: \"string\", desc: \"City name or zip code\"\n param :unit, type: \"string\", desc: \"Temperature unit\", required: false\n\n def execute(location:, unit: \"celsius\")\n WeatherService.current(location, unit: unit)\n end\nend\n</code></pre>"},{"location":"api/core/tool/#factory-pattern","title":"Factory Pattern","text":"<pre><code>tool = RobotLab::Tool.create(\n name: \"get_weather\",\n description: \"Get current weather for a location\",\n parameters: {\n type: \"object\",\n properties: {\n location: { type: \"string\", description: \"City name\" }\n },\n required: [\"location\"]\n }\n) { |args| WeatherService.current(args[:location]) }\n</code></pre>"},{"location":"api/core/tool/#constructor","title":"Constructor","text":"<pre><code>Tool.new(robot: nil)\n</code></pre> <p>Parameters:</p> Name Type Description <code>robot</code> <code>Robot, nil</code> The owning robot instance"},{"location":"api/core/tool/#class-methods","title":"Class Methods","text":""},{"location":"api/core/tool/#raise_on_error-raise_on_error","title":"raise_on_error / raise_on_error?","text":"<pre><code>MyTool.raise_on_error = true\nMyTool.raise_on_error? # =&gt; true\n</code></pre> <p>Per-class flag controlling whether <code>call</code> propagates exceptions from <code>execute</code> instead of catching them. Defaults to <code>false</code>. Does not affect other tool classes.</p>"},{"location":"api/core/tool/#toolcreate","title":"Tool.create","text":"<p>Factory for dynamic tools (MCP wrappers, inline tools).</p> <pre><code>Tool.create(\n name:,\n description: nil,\n parameters: nil,\n mcp: nil,\n robot: nil,\n &amp;handler\n)\n</code></pre> <p>Parameters:</p> Name Type Description <code>name</code> <code>String, Symbol</code> Tool identifier <code>description</code> <code>String</code> What the tool does <code>parameters</code> <code>Hash</code> JSON Schema parameter definition <code>mcp</code> <code>String</code> MCP server name <code>robot</code> <code>Robot</code> Owning robot instance <code>&amp;handler</code> <code>Block</code> Receives <code>args</code> hash, returns result"},{"location":"api/core/tool/#inherited-dsl-from-rubyllmtool","title":"Inherited DSL (from RubyLLM::Tool)","text":""},{"location":"api/core/tool/#description","title":"description","text":"<pre><code>class MyTool &lt; RobotLab::Tool\n description \"What this tool does\"\nend\n</code></pre>"},{"location":"api/core/tool/#param","title":"param","text":"<pre><code>class MyTool &lt; RobotLab::Tool\n param :name, type: \"string\", desc: \"User's name\"\n param :age, type: \"integer\", desc: \"User's age\", required: false\nend\n</code></pre>"},{"location":"api/core/tool/#execute","title":"execute","text":"<pre><code>class MyTool &lt; RobotLab::Tool\n def execute(name:, age: nil)\n # Implementation\n end\nend\n</code></pre>"},{"location":"api/core/tool/#halt","title":"halt","text":"<p>Stop the tool use loop from within execute:</p> <pre><code>def execute(**)\n halt(\"Done processing\")\nend\n</code></pre>"},{"location":"api/core/tool/#with_params","title":"with_params","text":"<p>Set provider-specific parameters:</p> <pre><code>class MyTool &lt; RobotLab::Tool\n with_params(strict: true)\nend\n</code></pre>"},{"location":"api/core/tool/#attributes","title":"Attributes","text":""},{"location":"api/core/tool/#robot","title":"robot","text":"<pre><code>tool.robot # =&gt; Robot or nil\ntool.robot = some_robot\n</code></pre> <p>Read/write accessor for the owning robot. Set via constructor or assigned later.</p>"},{"location":"api/core/tool/#mcp","title":"mcp","text":"<pre><code>tool.mcp # =&gt; String or nil\n</code></pre> <p>The MCP server name, set via <code>Tool.create(mcp: \"server_name\")</code>.</p>"},{"location":"api/core/tool/#methods","title":"Methods","text":""},{"location":"api/core/tool/#name","title":"name","text":"<pre><code>tool.name # =&gt; String\n</code></pre> <p>Returns the tool name. For subclasses, derived from the class name (CamelCase to snake_case). For <code>create</code>d tools, returns the explicit name.</p>"},{"location":"api/core/tool/#mcp_1","title":"mcp?","text":"<pre><code>tool.mcp? # =&gt; Boolean\n</code></pre> <p>Whether this is an MCP-provided tool.</p>"},{"location":"api/core/tool/#call","title":"call","text":"<pre><code>result = tool.call(args_hash)\n</code></pre> <p>Overrides <code>RubyLLM::Tool#call</code> with graceful error handling. Converts string keys to symbols and calls <code>execute(**args)</code>. If <code>execute</code> raises a <code>StandardError</code>, the error is caught and returned as a plain-text string the LLM can reason about:</p> <pre><code>Error (tool_name): exception message\n</code></pre> <p>The error is also logged via <code>RobotLab.config.logger</code> at <code>:warn</code> level.</p> <p>To propagate exceptions instead of catching them (for critical tools), set <code>raise_on_error</code> on the class:</p> <pre><code>class CriticalTool &lt; RobotLab::Tool\n self.raise_on_error = true\n # ...\nend\n</code></pre> <p><code>raise_on_error</code> is per-class (defaults to <code>false</code>) and does not affect other tool classes.</p>"},{"location":"api/core/tool/#params_schema","title":"params_schema","text":"<pre><code>tool.params_schema # =&gt; Hash or nil\n</code></pre> <p>Inherited. Returns the JSON Schema for tool parameters.</p>"},{"location":"api/core/tool/#provider_params","title":"provider_params","text":"<pre><code>tool.provider_params # =&gt; Hash\n</code></pre> <p>Inherited. Returns provider-specific parameters (e.g., <code>{ strict: true }</code>).</p>"},{"location":"api/core/tool/#to_h","title":"to_h","text":"<pre><code>tool.to_h # =&gt; Hash\n</code></pre> <p>Hash representation with <code>:name</code>, <code>:description</code>, <code>:mcp</code>.</p>"},{"location":"api/core/tool/#to_json","title":"to_json","text":"<pre><code>tool.to_json # =&gt; String\n</code></pre> <p>JSON representation.</p>"},{"location":"api/core/tool/#to_json_schema","title":"to_json_schema","text":"<pre><code>tool.to_json_schema # =&gt; Hash\n</code></pre> <p>JSON Schema representation for LLM function calling. Returns <code>{ name:, description:, parameters: }</code>.</p>"},{"location":"api/core/tool/#robot-aware-tools","title":"Robot-Aware Tools","text":"<p>Tools that modify their owning robot use the <code>robot</code> accessor:</p> <pre><code>class AdjustTemperature &lt; RobotLab::Tool\n description \"Adjust the robot's creativity level\"\n\n param :level, type: \"number\", desc: \"Temperature from 0.0 to 1.0\"\n\n def execute(level:)\n robot.with_temperature(level)\n \"Temperature adjusted to #{level}\"\n end\nend\n\n# Pass robot: self when constructing\nrobot = RobotLab.build(\n name: \"creative_bot\",\n system_prompt: \"You are creative.\",\n local_tools: [AdjustTemperature.new(robot: self)]\n)\n</code></pre>"},{"location":"api/core/tool/#parameter-types","title":"Parameter Types","text":""},{"location":"api/core/tool/#string","title":"String","text":"<pre><code>param :name, type: \"string\", desc: \"User's full name\"\n</code></pre>"},{"location":"api/core/tool/#integer","title":"Integer","text":"<pre><code>param :count, type: \"integer\", desc: \"Number of results\"\n</code></pre>"},{"location":"api/core/tool/#number-float","title":"Number (Float)","text":"<pre><code>param :price, type: \"number\", desc: \"Price in dollars\"\n</code></pre>"},{"location":"api/core/tool/#boolean","title":"Boolean","text":"<pre><code>param :active, type: \"boolean\", desc: \"Whether the user is active\"\n</code></pre>"},{"location":"api/core/tool/#required-vs-optional","title":"Required vs Optional","text":"<p>Parameters are required by default. Mark optional with <code>required: false</code>:</p> <pre><code>param :query, type: \"string\", desc: \"Search query\" # required\nparam :limit, type: \"integer\", desc: \"Max results\", required: false # optional\n</code></pre>"},{"location":"api/core/tool/#built-in-askuser","title":"Built-in: AskUser","text":"<p><code>RobotLab::AskUser</code> is a built-in tool that lets a robot ask the user a question via the terminal. The LLM decides when human input is needed and calls this tool.</p>"},{"location":"api/core/tool/#class-robotlabaskuser-robotlabtool","title":"Class: <code>RobotLab::AskUser &lt; RobotLab::Tool</code>","text":"<pre><code>class RobotLab::AskUser &lt; RobotLab::Tool\n description \"Ask the user a question and wait for their typed response\"\n param :question, type: \"string\", desc: \"The question to ask the user\"\n param :choices, type: \"array\", desc: \"Optional list of choices to present\", required: false\n param :default, type: \"string\", desc: \"Default value if user presses Enter\", required: false\nend\n</code></pre>"},{"location":"api/core/tool/#parameters","title":"Parameters","text":"Name Type Required Description <code>question</code> <code>String</code> Yes The question to display <code>choices</code> <code>Array</code> No Numbered choices to present <code>default</code> <code>String</code> No Value returned when user presses Enter without typing"},{"location":"api/core/tool/#io-resolution","title":"IO Resolution","text":"<p>The tool reads input and writes output using the owning robot's <code>input</code>/<code>output</code> accessors:</p> <ol> <li><code>robot.input</code> / <code>robot.output</code> if set</li> <li>Falls back to <code>$stdin</code> / <code>$stdout</code></li> </ol>"},{"location":"api/core/tool/#terminal-output","title":"Terminal Output","text":"<pre><code>[robot_name] What programming language do you want to learn?\n 1. Ruby\n 2. Python\n 3. Go\n&gt; [Ruby]\n</code></pre>"},{"location":"api/core/tool/#usage","title":"Usage","text":"<pre><code>robot = RobotLab.build(\n name: \"interviewer\",\n system_prompt: \"Interview the user about their project needs. Use ask_user to gather information.\",\n local_tools: [RobotLab::AskUser]\n)\nrobot.run(\"Find out what the user wants to build\")\n</code></pre>"},{"location":"api/core/tool/#testing-with-stringio","title":"Testing with StringIO","text":"<pre><code>robot = RobotLab::Robot.new(name: \"bot\", template: :assistant)\nrobot.input = StringIO.new(\"Ruby\\n\")\nrobot.output = StringIO.new\n\ntool = RobotLab::AskUser.new(robot: robot)\nresult = tool.call(\"question\" =&gt; \"Pick a language:\", \"choices\" =&gt; [\"Ruby\", \"Python\"])\n# =&gt; \"Ruby\"\n</code></pre>"},{"location":"api/core/tool/#choice-mapping","title":"Choice Mapping","text":"<p>When <code>choices</code> are provided, the user can type either:</p> <ul> <li>A number (e.g., <code>2</code>) \u2014 mapped to the corresponding choice text</li> <li>Text (e.g., <code>Python</code>) \u2014 returned as-is</li> </ul> <p>Out-of-range numbers are returned as-is (the LLM can re-ask if needed).</p>"},{"location":"api/core/tool/#see-also","title":"See Also","text":"<ul> <li>Using Tools Guide</li> <li>Robot</li> <li>MCP Integration</li> </ul>"},{"location":"api/mcp/","title":"MCP (Model Context Protocol)","text":"<p>Integration with MCP servers for extended tool capabilities.</p>"},{"location":"api/mcp/#overview","title":"Overview","text":"<p>MCP allows robots to connect to external tool servers, extending their capabilities without modifying robot code. RobotLab provides an MCP client that communicates with MCP-compliant servers over multiple transport types.</p> <pre><code>robot = Robot.new(\n name: \"developer\",\n system_prompt: \"You help with coding tasks.\",\n mcp: [\n {\n name: \"filesystem\",\n transport: { type: \"stdio\", command: \"npx @modelcontextprotocol/server-filesystem\" }\n }\n ]\n)\n</code></pre>"},{"location":"api/mcp/#components","title":"Components","text":"Component Description Client Connects to MCP servers, lists tools, calls tools Server Server configuration data structure Transports Communication methods (stdio, WebSocket, SSE, HTTP)"},{"location":"api/mcp/#quick-start","title":"Quick Start","text":""},{"location":"api/mcp/#using-mcp-with-a-robot","title":"Using MCP with a Robot","text":"<p>Pass MCP server configurations via the <code>mcp:</code> parameter when creating a robot:</p> <pre><code>robot = Robot.new(\n name: \"assistant\",\n template: :assistant,\n mcp: [\n { name: \"github\", transport: { type: \"stdio\", command: \"mcp-server-github\" } }\n ]\n)\n\nresult = robot.run(\"List my open pull requests\")\nresult.last_text_content\n</code></pre>"},{"location":"api/mcp/#mcp-in-networks","title":"MCP in Networks","text":"<p>Robots in a network can inherit MCP servers from the network or define their own:</p> <pre><code>network_mcp = [\n { name: \"github\", transport: { type: \"stdio\", command: \"mcp-server-github\" } }\n]\n\nrobot = Robot.new(\n name: \"assistant\",\n template: :assistant,\n mcp: :inherit # Use network's MCP servers\n)\n</code></pre>"},{"location":"api/mcp/#direct-client-usage","title":"Direct Client Usage","text":"<pre><code>client = RobotLab::MCP::Client.new(\n name: \"filesystem\",\n transport: { type: \"stdio\", command: \"mcp-server-filesystem\", args: [\"--root\", \"/data\"] }\n)\n\nclient.connect\ntools = client.list_tools\nresult = client.call_tool(\"readFile\", { path: \"/data/config.yml\" })\nclient.disconnect\n</code></pre>"},{"location":"api/mcp/#transport-types","title":"Transport Types","text":"Type Config Key Use Case <code>stdio</code> <code>\"stdio\"</code> Local command/subprocess execution <code>websocket</code> <code>\"ws\"</code> or <code>\"websocket\"</code> Real-time bidirectional communication <code>sse</code> <code>\"sse\"</code> Server-sent events streaming <code>streamable-http</code> <code>\"streamable-http\"</code> or <code>\"http\"</code> HTTP request/response with session support"},{"location":"api/mcp/#mcp-parameter-values","title":"MCP Parameter Values","text":"<p>The <code>mcp:</code> parameter on a Robot accepts three types of values:</p> Value Meaning <code>:none</code> No MCP servers (explicitly disabled) <code>:inherit</code> Use the network's MCP servers <code>Array&lt;Hash&gt;</code> Explicit list of server configurations <p>Each server configuration hash requires:</p> Key Type Description <code>name</code> <code>String</code> Unique server identifier <code>transport</code> <code>Hash</code> Transport configuration (must include <code>type</code>)"},{"location":"api/mcp/#error-handling","title":"Error Handling","text":"<p>MCP operations raise <code>RobotLab::MCPError</code> when:</p> <ul> <li>Connection to a server fails</li> <li>A request is made without an active connection</li> <li>An unsupported transport type is specified</li> </ul> <pre><code>begin\n client.connect\n client.call_tool(\"unknown_tool\", {})\nrescue RobotLab::MCPError =&gt; e\n puts \"MCP error: #{e.message}\"\nend\n</code></pre>"},{"location":"api/mcp/#see-also","title":"See Also","text":"<ul> <li>MCP Client</li> <li>MCP Server</li> <li>Transports</li> </ul>"},{"location":"api/mcp/client/","title":"MCP Client","text":"<p>Connects to MCP servers, discovers tools, and invokes them via the Model Context Protocol.</p>"},{"location":"api/mcp/client/#class-robotlabmcpclient","title":"Class: <code>RobotLab::MCP::Client</code>","text":"<pre><code>client = RobotLab::MCP::Client.new(\n name: \"filesystem\",\n transport: { type: \"stdio\", command: \"mcp-server-filesystem\", args: [\"--root\", \"/data\"] }\n)\n\nclient.connect\ntools = client.list_tools\nresult = client.call_tool(\"readFile\", { path: \"/data/readme.txt\" })\nclient.disconnect\n</code></pre>"},{"location":"api/mcp/client/#constructor","title":"Constructor","text":"<pre><code>Client.new(server_or_config)\n</code></pre> <p>Accepts either a <code>Server</code> instance or a Hash configuration. When a Hash is provided, it is used to construct a <code>Server</code> internally.</p> <p>Parameters:</p> Name Type Description <code>server_or_config</code> <code>Server</code>, <code>Hash</code> Server instance or configuration hash <p>Hash Configuration Keys:</p> Key Type Required Description <code>name</code> <code>String</code> Yes Server identifier <code>transport</code> <code>Hash</code> Yes Transport configuration (must include <code>type</code>) <p>Raises: <code>ArgumentError</code> if the config is neither a <code>Server</code> nor a <code>Hash</code>.</p>"},{"location":"api/mcp/client/#attributes","title":"Attributes","text":""},{"location":"api/mcp/client/#server","title":"server","text":"<pre><code>client.server # =&gt; RobotLab::MCP::Server\n</code></pre> <p>The MCP server configuration object.</p>"},{"location":"api/mcp/client/#connected","title":"connected?","text":"<pre><code>client.connected? # =&gt; Boolean\n</code></pre> <p>Whether the client is currently connected to the server.</p>"},{"location":"api/mcp/client/#methods","title":"Methods","text":""},{"location":"api/mcp/client/#connect","title":"connect","text":"<pre><code>client.connect # =&gt; self\n</code></pre> <p>Establish a connection to the MCP server. Creates the appropriate transport based on the server's transport type, then connects. If already connected, returns immediately.</p> <p>Connection failures are logged as warnings and the client remains in a disconnected state (does not raise).</p>"},{"location":"api/mcp/client/#disconnect","title":"disconnect","text":"<pre><code>client.disconnect # =&gt; self\n</code></pre> <p>Close the connection to the MCP server. Closes the underlying transport and resets connection state. If not connected, returns immediately.</p>"},{"location":"api/mcp/client/#list_tools","title":"list_tools","text":"<pre><code>client.list_tools # =&gt; Array&lt;Hash&gt;\n</code></pre> <p>Discover available tools from the server. Returns an array of tool definition hashes.</p> <p>Raises: <code>MCPError</code> if not connected.</p>"},{"location":"api/mcp/client/#call_tool","title":"call_tool","text":"<pre><code>result = client.call_tool(name, arguments = {})\n</code></pre> <p>Execute a tool on the server.</p> <p>Parameters:</p> Name Type Description <code>name</code> <code>String</code> Tool name <code>arguments</code> <code>Hash</code> Tool arguments (default: <code>{}</code>) <p>Returns: Tool result content (from the <code>content</code> field of the response).</p> <p>Raises: <code>MCPError</code> if not connected.</p>"},{"location":"api/mcp/client/#list_resources","title":"list_resources","text":"<pre><code>client.list_resources # =&gt; Array&lt;Hash&gt;\n</code></pre> <p>List available resources from the server.</p> <p>Raises: <code>MCPError</code> if not connected.</p>"},{"location":"api/mcp/client/#read_resource","title":"read_resource","text":"<pre><code>client.read_resource(uri) # =&gt; Object\n</code></pre> <p>Read a resource by URI.</p> <p>Parameters:</p> Name Type Description <code>uri</code> <code>String</code> Resource URI <p>Raises: <code>MCPError</code> if not connected.</p>"},{"location":"api/mcp/client/#list_prompts","title":"list_prompts","text":"<pre><code>client.list_prompts # =&gt; Array&lt;Hash&gt;\n</code></pre> <p>List available prompts from the server.</p> <p>Raises: <code>MCPError</code> if not connected.</p>"},{"location":"api/mcp/client/#get_prompt","title":"get_prompt","text":"<pre><code>client.get_prompt(name, arguments = {}) # =&gt; Hash\n</code></pre> <p>Get a prompt by name with optional arguments.</p> <p>Parameters:</p> Name Type Description <code>name</code> <code>String</code> Prompt name <code>arguments</code> <code>Hash</code> Prompt arguments (default: <code>{}</code>) <p>Raises: <code>MCPError</code> if not connected.</p>"},{"location":"api/mcp/client/#to_h","title":"to_h","text":"<pre><code>client.to_h # =&gt; Hash\n</code></pre> <p>Converts the client to a hash representation containing server config and connection status.</p>"},{"location":"api/mcp/client/#transport-configuration","title":"Transport Configuration","text":"<p>The transport type is determined by the <code>type</code> key in the transport hash of the <code>Server</code> configuration.</p>"},{"location":"api/mcp/client/#stdio","title":"Stdio","text":"<pre><code>client = RobotLab::MCP::Client.new(\n name: \"local\",\n transport: {\n type: \"stdio\",\n command: \"npx\",\n args: [\"@modelcontextprotocol/server-filesystem\", \"/path\"]\n }\n)\n</code></pre>"},{"location":"api/mcp/client/#websocket","title":"WebSocket","text":"<pre><code>client = RobotLab::MCP::Client.new(\n name: \"remote\",\n transport: {\n type: \"ws\",\n url: \"wss://mcp.example.com/ws\"\n }\n)\n</code></pre>"},{"location":"api/mcp/client/#sse","title":"SSE","text":"<pre><code>client = RobotLab::MCP::Client.new(\n name: \"streaming\",\n transport: {\n type: \"sse\",\n url: \"https://mcp.example.com/sse\"\n }\n)\n</code></pre>"},{"location":"api/mcp/client/#streamable-http","title":"Streamable HTTP","text":"<pre><code>client = RobotLab::MCP::Client.new(\n name: \"http\",\n transport: {\n type: \"streamable-http\",\n url: \"https://mcp.example.com/mcp\",\n session_id: \"optional-session-id\"\n }\n)\n</code></pre>"},{"location":"api/mcp/client/#examples","title":"Examples","text":""},{"location":"api/mcp/client/#basic-usage","title":"Basic Usage","text":"<pre><code>client = RobotLab::MCP::Client.new(\n name: \"github\",\n transport: { type: \"stdio\", command: \"mcp-server-github\" }\n)\n\nclient.connect\n\n# List available tools\ntools = client.list_tools\ntools.each { |t| puts \"#{t[:name]}: #{t[:description]}\" }\n\n# Call a tool\nresult = client.call_tool(\"search_repositories\", { query: \"ruby mcp\" })\nputs result\n\nclient.disconnect\n</code></pre>"},{"location":"api/mcp/client/#from-a-server-object","title":"From a Server Object","text":"<pre><code>server = RobotLab::MCP::Server.new(\n name: \"neon\",\n transport: { type: \"ws\", url: \"ws://localhost:8080\" }\n)\n\nclient = RobotLab::MCP::Client.new(server)\nclient.connect\n</code></pre>"},{"location":"api/mcp/client/#in-a-robot","title":"In a Robot","text":"<pre><code>robot = Robot.new(\n name: \"assistant\",\n system_prompt: \"You help with file operations.\",\n mcp: [\n { name: \"fs\", transport: { type: \"stdio\", command: \"mcp-fs\" } }\n ]\n)\n\n# MCP tools are automatically discovered and available to the LLM\nresult = robot.run(\"Read the contents of /data/config.yml\")\nputs result.last_text_content\n</code></pre>"},{"location":"api/mcp/client/#error-handling","title":"Error Handling","text":"<pre><code>begin\n client.connect\n result = client.call_tool(\"unknown_tool\", {})\nrescue RobotLab::MCPError =&gt; e\n puts \"MCP error: #{e.message}\"\nensure\n client.disconnect\nend\n</code></pre>"},{"location":"api/mcp/client/#see-also","title":"See Also","text":"<ul> <li>MCP Overview</li> <li>Server Configuration</li> <li>Transports</li> </ul>"},{"location":"api/mcp/server/","title":"MCP Server Configuration","text":"<p>Data structure for MCP server connection configuration.</p>"},{"location":"api/mcp/server/#class-robotlabmcpserver","title":"Class: <code>RobotLab::MCP::Server</code>","text":"<p><code>Server</code> is a configuration object that defines how to connect to an MCP server. It holds the server name and transport settings, and validates the configuration on construction.</p> <p>This is not an MCP server implementation -- it is the configuration used by <code>MCP::Client</code> to establish a connection to an external MCP server.</p> <pre><code>server = RobotLab::MCP::Server.new(\n name: \"filesystem\",\n transport: { type: \"stdio\", command: \"mcp-server-filesystem\", args: [\"--root\", \"/data\"] }\n)\n</code></pre>"},{"location":"api/mcp/server/#constructor","title":"Constructor","text":"<pre><code>Server.new(name:, transport:)\n</code></pre> <p>Parameters:</p> Name Type Description <code>name</code> <code>String</code> Unique server identifier <code>transport</code> <code>Hash</code> Transport configuration (must include <code>type</code>) <p>Raises: <code>ArgumentError</code> if: - The transport type is not one of the valid types - A stdio transport is missing the <code>:command</code> key - A network transport (ws, websocket, sse, streamable-http, http) is missing the <code>:url</code> key</p>"},{"location":"api/mcp/server/#valid-transport-types","title":"Valid Transport Types","text":"<pre><code>RobotLab::MCP::Server::VALID_TRANSPORT_TYPES\n# =&gt; [\"stdio\", \"sse\", \"ws\", \"websocket\", \"streamable-http\", \"http\"]\n</code></pre>"},{"location":"api/mcp/server/#attributes","title":"Attributes","text":""},{"location":"api/mcp/server/#name","title":"name","text":"<pre><code>server.name # =&gt; String\n</code></pre> <p>The server identifier string.</p>"},{"location":"api/mcp/server/#transport","title":"transport","text":"<pre><code>server.transport # =&gt; Hash\n</code></pre> <p>The normalized transport configuration hash (keys are symbols, type is downcased).</p>"},{"location":"api/mcp/server/#methods","title":"Methods","text":""},{"location":"api/mcp/server/#transport_type","title":"transport_type","text":"<pre><code>server.transport_type # =&gt; String\n</code></pre> <p>Returns the transport type string (e.g., <code>\"stdio\"</code>, <code>\"ws\"</code>, <code>\"sse\"</code>).</p>"},{"location":"api/mcp/server/#to_h","title":"to_h","text":"<pre><code>server.to_h # =&gt; { name: \"...\", transport: { ... } }\n</code></pre> <p>Converts the server configuration to a hash representation.</p>"},{"location":"api/mcp/server/#transport-configuration-options","title":"Transport Configuration Options","text":""},{"location":"api/mcp/server/#stdio-transport","title":"Stdio Transport","text":"<p>For local MCP servers running as subprocesses:</p> <pre><code>Server.new(\n name: \"filesystem\",\n transport: {\n type: \"stdio\",\n command: \"mcp-server-filesystem\", # Required\n args: [\"--root\", \"/data\"], # Optional\n env: { \"DEBUG\" =&gt; \"true\" } # Optional\n }\n)\n</code></pre> Key Type Required Description <code>type</code> <code>String</code> Yes Must be <code>\"stdio\"</code> <code>command</code> <code>String</code> Yes Executable command <code>args</code> <code>Array&lt;String&gt;</code> No Command arguments <code>env</code> <code>Hash</code> No Environment variables"},{"location":"api/mcp/server/#websocket-transport","title":"WebSocket Transport","text":"<p>For bidirectional real-time communication:</p> <pre><code>Server.new(\n name: \"neon\",\n transport: {\n type: \"ws\",\n url: \"ws://localhost:8080\" # Required\n }\n)\n</code></pre> Key Type Required Description <code>type</code> <code>String</code> Yes <code>\"ws\"</code> or <code>\"websocket\"</code> <code>url</code> <code>String</code> Yes WebSocket endpoint URL"},{"location":"api/mcp/server/#sse-transport","title":"SSE Transport","text":"<p>For server-sent events streaming:</p> <pre><code>Server.new(\n name: \"streaming\",\n transport: {\n type: \"sse\",\n url: \"http://localhost:8080/sse\" # Required\n }\n)\n</code></pre> Key Type Required Description <code>type</code> <code>String</code> Yes Must be <code>\"sse\"</code> <code>url</code> <code>String</code> Yes SSE endpoint URL"},{"location":"api/mcp/server/#streamable-http-transport","title":"Streamable HTTP Transport","text":"<p>For HTTP-based communication with session management:</p> <pre><code>Server.new(\n name: \"api\",\n transport: {\n type: \"streamable-http\",\n url: \"https://server.smithery.ai/neon/mcp\", # Required\n session_id: \"abc123\", # Optional\n auth_provider: -&gt; { \"Bearer #{token}\" } # Optional\n }\n)\n</code></pre> Key Type Required Description <code>type</code> <code>String</code> Yes <code>\"streamable-http\"</code> or <code>\"http\"</code> <code>url</code> <code>String</code> Yes HTTP endpoint URL <code>session_id</code> <code>String</code> No Session identifier <code>auth_provider</code> <code>Proc</code> No Authentication callback returning auth header value"},{"location":"api/mcp/server/#examples","title":"Examples","text":""},{"location":"api/mcp/server/#multiple-server-configurations","title":"Multiple Server Configurations","text":"<pre><code>servers = [\n {\n name: \"filesystem\",\n transport: { type: \"stdio\", command: \"mcp-server-filesystem\", args: [\"/data\"] }\n },\n {\n name: \"github\",\n transport: { type: \"stdio\", command: \"mcp-server-github\" }\n },\n {\n name: \"database\",\n transport: { type: \"ws\", url: \"ws://localhost:9090\" }\n }\n]\n\n# Pass directly to a robot\nrobot = Robot.new(\n name: \"dev_assistant\",\n system_prompt: \"You help with development tasks.\",\n mcp: servers\n)\n</code></pre>"},{"location":"api/mcp/server/#creating-a-client-from-a-server","title":"Creating a Client from a Server","text":"<pre><code>server = RobotLab::MCP::Server.new(\n name: \"tools\",\n transport: { type: \"ws\", url: \"ws://localhost:8080\" }\n)\n\nclient = RobotLab::MCP::Client.new(server)\nclient.connect\n</code></pre>"},{"location":"api/mcp/server/#see-also","title":"See Also","text":"<ul> <li>MCP Overview</li> <li>MCP Client</li> <li>Transports</li> </ul>"},{"location":"api/mcp/transports/","title":"MCP Transports","text":"<p>Communication methods for MCP client-server connections.</p>"},{"location":"api/mcp/transports/#overview","title":"Overview","text":"<p>Transports handle the low-level communication between <code>MCP::Client</code> and external MCP servers. All transports implement the same interface defined by <code>Transports::Base</code>, using JSON-RPC 2.0 for message exchange and the MCP protocol version <code>2024-11-05</code> for initialization.</p> <p>RobotLab provides four built-in transport types:</p> Transport Class Use Case Stdio <code>Transports::Stdio</code> Local subprocess servers WebSocket <code>Transports::WebSocket</code> Real-time bidirectional SSE <code>Transports::SSE</code> Server-sent events Streamable HTTP <code>Transports::StreamableHTTP</code> HTTP with session support"},{"location":"api/mcp/transports/#base-interface","title":"Base Interface","text":"<p>All transports inherit from <code>RobotLab::MCP::Transports::Base</code> and implement:</p> <pre><code>class RobotLab::MCP::Transports::Base\n attr_reader :config # =&gt; Hash (symbolized keys)\n\n def connect # Establish connection, returns self\n def send_request(message) # Send JSON-RPC message, returns Hash response\n def close # Close connection, returns self\n def connected? # Returns Boolean\nend\n</code></pre>"},{"location":"api/mcp/transports/#stdio-transport","title":"Stdio Transport","text":"<p>Class: <code>RobotLab::MCP::Transports::Stdio</code></p> <p>Spawns a subprocess and communicates via stdin/stdout using JSON-RPC messages (one per line). Automatically sends MCP <code>initialize</code> and <code>notifications/initialized</code> on connect.</p>"},{"location":"api/mcp/transports/#configuration","title":"Configuration","text":"<pre><code>{\n type: \"stdio\",\n command: \"mcp-server-filesystem\", # Required: executable command\n args: [\"--root\", \"/data\"], # Optional: command arguments\n env: { \"DEBUG\" =&gt; \"true\" } # Optional: environment variables\n}\n</code></pre> Key Type Required Description <code>command</code> <code>String</code> Yes Executable command to spawn <code>args</code> <code>Array&lt;String&gt;</code> No Command arguments <code>env</code> <code>Hash</code> No Environment variables (merged with current env)"},{"location":"api/mcp/transports/#behavior","title":"Behavior","text":"<ul> <li>Uses <code>Open3.popen3</code> to spawn the subprocess</li> <li>Writes JSON-RPC messages to stdin (one per line)</li> <li>Reads responses from stdout, skipping notifications (messages without <code>id</code>)</li> <li><code>connected?</code> returns <code>true</code> when the subprocess is alive</li> <li><code>close</code> terminates stdin, stdout, stderr, and kills the subprocess</li> </ul>"},{"location":"api/mcp/transports/#example","title":"Example","text":"<pre><code>transport = RobotLab::MCP::Transports::Stdio.new(\n command: \"mcp-server-filesystem\",\n args: [\"--root\", \"/data\"],\n env: { \"DEBUG\" =&gt; \"true\" }\n)\n\ntransport.connect\nresponse = transport.send_request({ jsonrpc: \"2.0\", id: 1, method: \"tools/list\" })\ntransport.close\n</code></pre>"},{"location":"api/mcp/transports/#websocket-transport","title":"WebSocket Transport","text":"<p>Class: <code>RobotLab::MCP::Transports::WebSocket</code></p> <p>Uses <code>async-websocket</code> for non-blocking bidirectional communication. Requires the <code>async-websocket</code> gem.</p>"},{"location":"api/mcp/transports/#configuration_1","title":"Configuration","text":"<pre><code>{\n type: \"ws\", # or \"websocket\"\n url: \"wss://mcp.example.com/ws\" # Required: WebSocket endpoint\n}\n</code></pre> Key Type Required Description <code>url</code> <code>String</code> Yes WebSocket endpoint URL"},{"location":"api/mcp/transports/#behavior_1","title":"Behavior","text":"<ul> <li>Uses <code>Async::WebSocket::Client.connect</code> within an <code>Async</code> block</li> <li>Sends JSON-RPC messages as JSON strings</li> <li>Reads responses synchronously within the async context</li> <li>Raises <code>MCPError</code> if the <code>async-websocket</code> gem is not installed</li> </ul>"},{"location":"api/mcp/transports/#example_1","title":"Example","text":"<pre><code>transport = RobotLab::MCP::Transports::WebSocket.new(\n url: \"ws://localhost:8080\"\n)\n\ntransport.connect\nresponse = transport.send_request({ jsonrpc: \"2.0\", id: 1, method: \"tools/list\" })\ntransport.close\n</code></pre>"},{"location":"api/mcp/transports/#sse-transport","title":"SSE Transport","text":"<p>Class: <code>RobotLab::MCP::Transports::SSE</code></p> <p>Uses <code>async-http</code> for HTTP-based communication. Sends requests via HTTP POST and reads responses. Requires the <code>async-http</code> gem.</p>"},{"location":"api/mcp/transports/#configuration_2","title":"Configuration","text":"<pre><code>{\n type: \"sse\",\n url: \"http://localhost:8080/sse\" # Required: SSE endpoint\n}\n</code></pre> Key Type Required Description <code>url</code> <code>String</code> Yes SSE/HTTP endpoint URL"},{"location":"api/mcp/transports/#behavior_2","title":"Behavior","text":"<ul> <li>Creates an <code>Async::HTTP::Client</code> on connect</li> <li>Sends JSON-RPC messages via HTTP POST with <code>Content-Type: application/json</code></li> <li>Reads and parses JSON response body</li> <li>Raises <code>MCPError</code> if the <code>async-http</code> gem is not installed</li> </ul>"},{"location":"api/mcp/transports/#example_2","title":"Example","text":"<pre><code>transport = RobotLab::MCP::Transports::SSE.new(\n url: \"http://localhost:8080/sse\"\n)\n\ntransport.connect\nresponse = transport.send_request({ jsonrpc: \"2.0\", id: 1, method: \"tools/list\" })\ntransport.close\n</code></pre>"},{"location":"api/mcp/transports/#streamable-http-transport","title":"Streamable HTTP Transport","text":"<p>Class: <code>RobotLab::MCP::Transports::StreamableHTTP</code></p> <p>HTTP-based transport with session management and optional authentication. Supports session IDs for maintaining server-side state across requests. Requires the <code>async-http</code> gem.</p>"},{"location":"api/mcp/transports/#configuration_3","title":"Configuration","text":"<pre><code>{\n type: \"streamable-http\", # or \"http\"\n url: \"https://server.smithery.ai/neon/mcp\", # Required: HTTP endpoint\n session_id: \"abc123\", # Optional: session identifier\n auth_provider: -&gt; { \"Bearer #{token}\" } # Optional: auth callback\n}\n</code></pre> Key Type Required Description <code>url</code> <code>String</code> Yes HTTP endpoint URL <code>session_id</code> <code>String</code> No Pre-existing session identifier <code>auth_provider</code> <code>Proc</code> No Callback returning Authorization header value"},{"location":"api/mcp/transports/#behavior_3","title":"Behavior","text":"<ul> <li>Creates an <code>Async::HTTP::Client</code> on connect</li> <li>Sends MCP <code>initialize</code> on connect; if no session ID was provided, extracts it from the server response (<code>serverInfo.sessionId</code>)</li> <li>Sends <code>X-Session-ID</code> header with each request when a session ID is available</li> <li>Calls <code>auth_provider</code> for each request to populate the <code>Authorization</code> header</li> <li>Exposes <code>session_id</code> reader for accessing the current session ID</li> <li>Raises <code>MCPError</code> if the <code>async-http</code> gem is not installed</li> </ul>"},{"location":"api/mcp/transports/#example_3","title":"Example","text":"<pre><code>transport = RobotLab::MCP::Transports::StreamableHTTP.new(\n url: \"https://server.smithery.ai/neon/mcp\",\n auth_provider: -&gt; { \"Bearer #{ENV['MCP_TOKEN']}\" }\n)\n\ntransport.connect\nputs transport.session_id # =&gt; assigned by server or pre-configured\n\nresponse = transport.send_request({ jsonrpc: \"2.0\", id: 1, method: \"tools/list\" })\ntransport.close\n</code></pre>"},{"location":"api/mcp/transports/#connection-lifecycle","title":"Connection Lifecycle","text":"<p>All transports follow the same lifecycle:</p> <ol> <li>Create -- instantiate with configuration hash</li> <li>Connect -- establish connection and perform MCP protocol initialization</li> <li>Request/Response -- send JSON-RPC requests, receive responses</li> <li>Close -- tear down connection and release resources</li> </ol> <p>Each transport sends the MCP <code>initialize</code> message during connect:</p> <pre><code>{\n \"jsonrpc\": \"2.0\",\n \"id\": 0,\n \"method\": \"initialize\",\n \"params\": {\n \"protocolVersion\": \"2024-11-05\",\n \"capabilities\": {},\n \"clientInfo\": {\n \"name\": \"RobotLab\",\n \"version\": \"&lt;current version&gt;\"\n }\n }\n}\n</code></pre>"},{"location":"api/mcp/transports/#error-handling","title":"Error Handling","text":"<p>All transports raise <code>RobotLab::MCPError</code> for connection and communication failures:</p> <pre><code>begin\n transport.connect\n transport.send_request(message)\nrescue RobotLab::MCPError =&gt; e\n puts \"Transport error: #{e.message}\"\nensure\n transport.close\nend\n</code></pre> <p>Specific error cases: - Not connected -- calling <code>send_request</code> before <code>connect</code> raises <code>MCPError</code> - Missing gem -- WebSocket, SSE, and HTTP transports raise <code>MCPError</code> with a <code>LoadError</code> message if required gems are not installed - No response -- Stdio transport raises <code>MCPError</code> if the subprocess produces no output</p>"},{"location":"api/mcp/transports/#see-also","title":"See Also","text":"<ul> <li>MCP Overview</li> <li>MCP Client</li> <li>Server Configuration</li> </ul>"},{"location":"api/messages/","title":"Messages","text":"<p>Message types for LLM conversation representation.</p>"},{"location":"api/messages/#overview","title":"Overview","text":"<p>RobotLab uses a structured message system to represent conversations between users, assistants, and tools.</p> <pre><code># User input\nuser_msg = UserMessage.new(\"Hello\", session_id: \"123\")\n\n# Assistant response\ntext_msg = TextMessage.new(role: \"assistant\", content: \"Hi there!\")\n\n# Tool interaction\ntool = ToolMessage.new(id: \"call_1\", name: \"get_weather\", input: { city: \"NYC\" })\ntool_call = ToolCallMessage.new(role: \"assistant\", tools: [tool])\ntool_result = ToolResultMessage.new(tool: tool, content: { data: { temp: 72 } })\n</code></pre>"},{"location":"api/messages/#message-hierarchy","title":"Message Hierarchy","text":"<pre><code>Message (base)\n\u251c\u2500\u2500 TextMessage - role + text content\n\u251c\u2500\u2500 ToolCallMessage - role + Array&lt;ToolMessage&gt;\n\u2514\u2500\u2500 ToolResultMessage - tool + result content\n\nUserMessage - Standalone (not a Message subclass)\nToolMessage - Standalone (not a Message subclass)\n</code></pre>"},{"location":"api/messages/#common-interface","title":"Common Interface","text":"<p>All Message subclasses implement:</p> <pre><code>message.role # =&gt; String (\"user\", \"assistant\", \"tool_result\")\nmessage.content # =&gt; String or structured data\nmessage.type # =&gt; String (\"text\", \"tool_call\", \"tool_result\")\nmessage.to_h # =&gt; Hash representation\nmessage.to_json # =&gt; JSON string\n</code></pre> <p>Type and role predicates:</p> <pre><code>message.text? # =&gt; true if type is \"text\"\nmessage.tool_call? # =&gt; true if type is \"tool_call\"\nmessage.tool_result? # =&gt; true if type is \"tool_result\"\nmessage.system? # =&gt; true if role is \"system\"\nmessage.user? # =&gt; true if role is \"user\"\nmessage.assistant? # =&gt; true if role is \"assistant\"\nmessage.stopped? # =&gt; true if stop_reason is \"stop\"\nmessage.tool_stop? # =&gt; true if stop_reason is \"tool\"\n</code></pre>"},{"location":"api/messages/#classes","title":"Classes","text":"Class Description UserMessage User input with session and metadata TextMessage Text message with role (system, user, or assistant) ToolCallMessage Tool invocation request containing ToolMessage objects ToolResultMessage Tool execution result"},{"location":"api/messages/#usage-in-memory","title":"Usage in Memory","text":"<p>Messages are typically accessed through memory:</p> <pre><code>memory.messages # =&gt; Array&lt;Message&gt;\n\n# Format for LLM\nmemory.format_history # =&gt; Array&lt;Message&gt;\n</code></pre>"},{"location":"api/messages/#see-also","title":"See Also","text":"<ul> <li>Memory</li> <li>Message Flow Architecture</li> </ul>"},{"location":"api/messages/text-message/","title":"TextMessage","text":"<p>Text message from system, user, or assistant.</p>"},{"location":"api/messages/text-message/#class-robotlabtextmessage","title":"Class: <code>RobotLab::TextMessage</code>","text":"<pre><code>message = TextMessage.new(role: \"assistant\", content: \"Hello! How can I help you today?\")\n</code></pre>"},{"location":"api/messages/text-message/#constructor","title":"Constructor","text":"<pre><code>TextMessage.new(role:, content:, stop_reason: nil)\n</code></pre> <p>Parameters:</p> Name Type Description <code>role</code> <code>String</code> Message role (\"system\", \"user\", or \"assistant\") <code>content</code> <code>String</code> The text content <code>stop_reason</code> <code>String</code>, <code>nil</code> Stop reason (\"stop\" or \"tool\")"},{"location":"api/messages/text-message/#attributes","title":"Attributes","text":""},{"location":"api/messages/text-message/#content","title":"content","text":"<pre><code>message.content # =&gt; String\n</code></pre> <p>The text content.</p>"},{"location":"api/messages/text-message/#role","title":"role","text":"<pre><code>message.role # =&gt; \"assistant\"\n</code></pre> <p>Returns a String: <code>\"system\"</code>, <code>\"user\"</code>, or <code>\"assistant\"</code>.</p>"},{"location":"api/messages/text-message/#type","title":"type","text":"<pre><code>message.type # =&gt; \"text\"\n</code></pre> <p>Always returns <code>\"text\"</code>.</p>"},{"location":"api/messages/text-message/#stop_reason","title":"stop_reason","text":"<pre><code>message.stop_reason # =&gt; \"stop\" or nil\n</code></pre> <p>The stop reason, if any.</p>"},{"location":"api/messages/text-message/#methods","title":"Methods","text":""},{"location":"api/messages/text-message/#to_h","title":"to_h","text":"<pre><code>message.to_h # =&gt; Hash\n</code></pre> <p>Hash representation.</p> <p>Returns:</p> <pre><code>{\n type: \"text\",\n role: \"assistant\",\n content: \"Hello! How can I help you today?\",\n stop_reason: \"stop\"\n}\n</code></pre>"},{"location":"api/messages/text-message/#to_json","title":"to_json","text":"<pre><code>message.to_json # =&gt; String\n</code></pre> <p>JSON representation.</p>"},{"location":"api/messages/text-message/#predicates","title":"Predicates","text":"<pre><code>message.text? # =&gt; true\nmessage.tool_call? # =&gt; false\nmessage.assistant? # =&gt; true (if role is \"assistant\")\nmessage.user? # =&gt; false\nmessage.stopped? # =&gt; true (if stop_reason is \"stop\")\n</code></pre>"},{"location":"api/messages/text-message/#examples","title":"Examples","text":""},{"location":"api/messages/text-message/#system-message","title":"System Message","text":"<pre><code>message = TextMessage.new(role: \"system\", content: \"You are a helpful assistant\")\nmessage.system? # =&gt; true\n</code></pre>"},{"location":"api/messages/text-message/#user-message","title":"User Message","text":"<pre><code>message = TextMessage.new(role: \"user\", content: \"What's the weather?\")\nmessage.user? # =&gt; true\n</code></pre>"},{"location":"api/messages/text-message/#assistant-response","title":"Assistant Response","text":"<pre><code>message = TextMessage.new(\n role: \"assistant\",\n content: \"Your order has shipped!\",\n stop_reason: \"stop\"\n)\nmessage.assistant? # =&gt; true\nmessage.stopped? # =&gt; true\n</code></pre>"},{"location":"api/messages/text-message/#in-robot-results","title":"In Robot Results","text":"<pre><code>result = robot.run(\"Tell me a joke\")\n\n# The result is a TextMessage when the assistant replies with text\nif result.text?\n puts result.content\nend\n</code></pre>"},{"location":"api/messages/text-message/#filtering-text-content","title":"Filtering Text Content","text":"<pre><code># Get only text messages from memory\ntext_messages = memory.messages.select(&amp;:text?).map(&amp;:content)\n</code></pre>"},{"location":"api/messages/text-message/#see-also","title":"See Also","text":"<ul> <li>UserMessage</li> <li>ToolCallMessage</li> <li>Robot</li> </ul>"},{"location":"api/messages/tool-call-message/","title":"ToolCallMessage","text":"<p>Tool invocation request from the LLM.</p>"},{"location":"api/messages/tool-call-message/#class-robotlabtoolcallmessage","title":"Class: <code>RobotLab::ToolCallMessage</code>","text":"<pre><code>message = ToolCallMessage.new(\n role: \"assistant\",\n tools: [\n ToolMessage.new(id: \"call_abc123\", name: \"get_weather\", input: { city: \"New York\" })\n ]\n)\n</code></pre>"},{"location":"api/messages/tool-call-message/#constructor","title":"Constructor","text":"<pre><code>ToolCallMessage.new(role:, tools:, stop_reason: nil)\n</code></pre> <p>Parameters:</p> Name Type Description <code>role</code> <code>String</code> Message role (typically \"assistant\") <code>tools</code> <code>Array&lt;ToolMessage&gt;</code> Array of tool call objects <code>stop_reason</code> <code>String</code>, <code>nil</code> Stop reason (defaults to \"tool\")"},{"location":"api/messages/tool-call-message/#toolmessage","title":"ToolMessage","text":"<p>Each tool call is represented by a standalone <code>ToolMessage</code> object:</p> <pre><code>ToolMessage.new(id:, name:, input:)\n</code></pre> Name Type Description <code>id</code> <code>String</code> Unique call identifier <code>name</code> <code>String</code> Tool name <code>input</code> <code>Hash</code> Tool parameters"},{"location":"api/messages/tool-call-message/#attributes","title":"Attributes","text":""},{"location":"api/messages/tool-call-message/#tools","title":"tools","text":"<pre><code>message.tools # =&gt; Array&lt;ToolMessage&gt;\n</code></pre> <p>Array of <code>ToolMessage</code> objects representing the tool calls.</p>"},{"location":"api/messages/tool-call-message/#role","title":"role","text":"<pre><code>message.role # =&gt; \"assistant\"\n</code></pre> <p>Returns a String. The LLM initiates tool calls, so this is typically <code>\"assistant\"</code>.</p>"},{"location":"api/messages/tool-call-message/#type","title":"type","text":"<pre><code>message.type # =&gt; \"tool_call\"\n</code></pre> <p>Always returns <code>\"tool_call\"</code>.</p>"},{"location":"api/messages/tool-call-message/#content","title":"content","text":"<pre><code>message.content # =&gt; nil\n</code></pre> <p>Always <code>nil</code> for tool call messages (the tool data is in <code>tools</code>).</p>"},{"location":"api/messages/tool-call-message/#stop_reason","title":"stop_reason","text":"<pre><code>message.stop_reason # =&gt; \"tool\"\n</code></pre> <p>Defaults to <code>\"tool\"</code> indicating the conversation stopped for tool execution.</p>"},{"location":"api/messages/tool-call-message/#methods","title":"Methods","text":""},{"location":"api/messages/tool-call-message/#to_h","title":"to_h","text":"<pre><code>message.to_h # =&gt; Hash\n</code></pre> <p>Hash representation.</p> <p>Returns:</p> <pre><code>{\n type: \"tool_call\",\n role: \"assistant\",\n tools: [\n { type: \"tool\", id: \"call_abc123\", name: \"get_weather\", input: { city: \"New York\" } }\n ],\n stop_reason: \"tool\"\n}\n</code></pre>"},{"location":"api/messages/tool-call-message/#to_json","title":"to_json","text":"<pre><code>message.to_json # =&gt; String\n</code></pre> <p>JSON representation.</p>"},{"location":"api/messages/tool-call-message/#predicates","title":"Predicates","text":"<pre><code>message.tool_call? # =&gt; true\nmessage.text? # =&gt; false\nmessage.assistant? # =&gt; true\nmessage.tool_stop? # =&gt; true\n</code></pre>"},{"location":"api/messages/tool-call-message/#examples","title":"Examples","text":""},{"location":"api/messages/tool-call-message/#single-tool-call","title":"Single Tool Call","text":"<pre><code>tool = ToolMessage.new(\n id: \"call_1\",\n name: \"search_orders\",\n input: { user_id: \"123\", status: \"pending\" }\n)\n\ncall = ToolCallMessage.new(role: \"assistant\", tools: [tool])\n</code></pre>"},{"location":"api/messages/tool-call-message/#multiple-tool-calls","title":"Multiple Tool Calls","text":"<pre><code>tools = [\n ToolMessage.new(id: \"call_1\", name: \"get_weather\", input: { city: \"NYC\" }),\n ToolMessage.new(id: \"call_2\", name: \"get_time\", input: { timezone: \"EST\" })\n]\n\ncall = ToolCallMessage.new(role: \"assistant\", tools: tools)\ncall.tools.length # =&gt; 2\n</code></pre>"},{"location":"api/messages/tool-call-message/#processing-tool-calls","title":"Processing Tool Calls","text":"<pre><code>if message.tool_call?\n message.tools.each do |tool|\n puts \"Tool called: #{tool.name}\"\n puts \"Parameters: #{tool.input.inspect}\"\n end\nend\n</code></pre>"},{"location":"api/messages/tool-call-message/#in-tool-execution-flow","title":"In Tool Execution Flow","text":"<pre><code># LLM returns a tool call\ntool = ToolMessage.new(id: \"call_weather_1\", name: \"get_weather\", input: { city: \"Seattle\" })\ntool_call = ToolCallMessage.new(role: \"assistant\", tools: [tool])\n\n# Execute the tool and record the result\nresult_data = execute_tool(tool.name, tool.input)\n\ntool_result = ToolResultMessage.new(\n tool: tool,\n content: { data: result_data }\n)\n</code></pre>"},{"location":"api/messages/tool-call-message/#see-also","title":"See Also","text":"<ul> <li>ToolResultMessage</li> <li>Tool</li> <li>Using Tools Guide</li> </ul>"},{"location":"api/messages/tool-result-message/","title":"ToolResultMessage","text":"<p>Result from tool execution.</p>"},{"location":"api/messages/tool-result-message/#class-robotlabtoolresultmessage","title":"Class: <code>RobotLab::ToolResultMessage</code>","text":"<pre><code>tool = ToolMessage.new(id: \"call_abc123\", name: \"get_weather\", input: { city: \"NYC\" })\n\nmessage = ToolResultMessage.new(\n tool: tool,\n content: { data: { temperature: 72, conditions: \"sunny\" } }\n)\n</code></pre>"},{"location":"api/messages/tool-result-message/#constructor","title":"Constructor","text":"<pre><code>ToolResultMessage.new(tool:, content:, stop_reason: nil)\n</code></pre> <p>Parameters:</p> Name Type Description <code>tool</code> <code>ToolMessage</code> The tool call that was executed <code>content</code> <code>Hash</code> Result with <code>:data</code> key (success) or <code>:error</code> key (failure) <code>stop_reason</code> <code>String</code>, <code>nil</code> Stop reason (defaults to \"tool\")"},{"location":"api/messages/tool-result-message/#attributes","title":"Attributes","text":""},{"location":"api/messages/tool-result-message/#tool","title":"tool","text":"<pre><code>message.tool # =&gt; ToolMessage\n</code></pre> <p>The <code>ToolMessage</code> representing the tool call that produced this result. Provides access to <code>tool.id</code>, <code>tool.name</code>, and <code>tool.input</code>.</p>"},{"location":"api/messages/tool-result-message/#content","title":"content","text":"<pre><code>message.content # =&gt; Hash\n</code></pre> <p>The result content. Contains either a <code>:data</code> key (success) or an <code>:error</code> key (failure).</p>"},{"location":"api/messages/tool-result-message/#role","title":"role","text":"<pre><code>message.role # =&gt; \"tool_result\"\n</code></pre> <p>Always returns <code>\"tool_result\"</code>.</p>"},{"location":"api/messages/tool-result-message/#type","title":"type","text":"<pre><code>message.type # =&gt; \"tool_result\"\n</code></pre> <p>Always returns <code>\"tool_result\"</code>.</p>"},{"location":"api/messages/tool-result-message/#stop_reason","title":"stop_reason","text":"<pre><code>message.stop_reason # =&gt; \"tool\"\n</code></pre> <p>Defaults to <code>\"tool\"</code>.</p>"},{"location":"api/messages/tool-result-message/#methods","title":"Methods","text":""},{"location":"api/messages/tool-result-message/#success","title":"success?","text":"<pre><code>message.success? # =&gt; Boolean\n</code></pre> <p>Returns <code>true</code> if the content contains a <code>:data</code> key.</p>"},{"location":"api/messages/tool-result-message/#error","title":"error?","text":"<pre><code>message.error? # =&gt; Boolean\n</code></pre> <p>Returns <code>true</code> if the content contains an <code>:error</code> key.</p>"},{"location":"api/messages/tool-result-message/#data","title":"data","text":"<pre><code>message.data # =&gt; Object | nil\n</code></pre> <p>Returns the result data if successful, <code>nil</code> otherwise.</p>"},{"location":"api/messages/tool-result-message/#error_1","title":"error","text":"<pre><code>message.error # =&gt; String | nil\n</code></pre> <p>Returns the error message if there was an error, <code>nil</code> otherwise.</p>"},{"location":"api/messages/tool-result-message/#to_h","title":"to_h","text":"<pre><code>message.to_h # =&gt; Hash\n</code></pre> <p>Hash representation.</p> <p>Returns:</p> <pre><code>{\n type: \"tool_result\",\n role: \"tool_result\",\n tool: { type: \"tool\", id: \"call_abc123\", name: \"get_weather\", input: { city: \"NYC\" } },\n content: { data: { temperature: 72, conditions: \"sunny\" } },\n stop_reason: \"tool\"\n}\n</code></pre>"},{"location":"api/messages/tool-result-message/#to_json","title":"to_json","text":"<pre><code>message.to_json # =&gt; String\n</code></pre> <p>JSON representation.</p>"},{"location":"api/messages/tool-result-message/#predicates","title":"Predicates","text":"<pre><code>message.tool_result? # =&gt; true\nmessage.tool_call? # =&gt; false\nmessage.text? # =&gt; false\nmessage.tool_stop? # =&gt; true\n</code></pre>"},{"location":"api/messages/tool-result-message/#examples","title":"Examples","text":""},{"location":"api/messages/tool-result-message/#successful-result","title":"Successful Result","text":"<pre><code>tool = ToolMessage.new(id: \"call_1\", name: \"search_orders\", input: { user_id: \"123\" })\n\nresult = ToolResultMessage.new(\n tool: tool,\n content: { data: { order_id: \"ord_123\", status: \"shipped\" } }\n)\n\nresult.success? # =&gt; true\nresult.data # =&gt; { order_id: \"ord_123\", status: \"shipped\" }\n</code></pre>"},{"location":"api/messages/tool-result-message/#error-result","title":"Error Result","text":"<pre><code>tool = ToolMessage.new(id: \"call_order\", name: \"get_order\", input: { id: \"bad\" })\n\nresult = ToolResultMessage.new(\n tool: tool,\n content: { error: \"Order not found\" }\n)\n\nresult.error? # =&gt; true\nresult.error # =&gt; \"Order not found\"\nresult.data # =&gt; nil\n</code></pre>"},{"location":"api/messages/tool-result-message/#accessing-tool-information","title":"Accessing Tool Information","text":"<pre><code>result = ToolResultMessage.new(\n tool: ToolMessage.new(id: \"call_1\", name: \"get_weather\", input: { city: \"Berlin\" }),\n content: { data: { temperature: 15, unit: \"celsius\" } }\n)\n\nresult.tool.name # =&gt; \"get_weather\"\nresult.tool.id # =&gt; \"call_1\"\nresult.tool.input # =&gt; { city: \"Berlin\" }\nresult.data # =&gt; { temperature: 15, unit: \"celsius\" }\n</code></pre>"},{"location":"api/messages/tool-result-message/#matching-tool-calls-with-results","title":"Matching Tool Calls with Results","text":"<pre><code># Given a ToolCallMessage and its results\ntool_call_msg.tools.each do |tool|\n # Find the matching result\n matching_result = results.find { |r| r.tool.id == tool.id }\n\n if matching_result&amp;.success?\n puts \"#{tool.name}(#{tool.input}) =&gt; #{matching_result.data}\"\n elsif matching_result&amp;.error?\n puts \"#{tool.name} failed: #{matching_result.error}\"\n end\nend\n</code></pre>"},{"location":"api/messages/tool-result-message/#in-memory-history","title":"In Memory History","text":"<pre><code># Find all tool results from memory\ntool_results = memory.messages.select(&amp;:tool_result?)\n\ntool_results.each do |tr|\n if tr.success?\n puts \"#{tr.tool.name}: #{tr.data}\"\n else\n puts \"#{tr.tool.name} error: #{tr.error}\"\n end\nend\n</code></pre>"},{"location":"api/messages/tool-result-message/#see-also","title":"See Also","text":"<ul> <li>ToolCallMessage</li> <li>Tool</li> <li>Using Tools Guide</li> </ul>"},{"location":"api/messages/user-message/","title":"UserMessage","text":"<p>User input with conversation metadata.</p>"},{"location":"api/messages/user-message/#class-robotlabusermessage","title":"Class: <code>RobotLab::UserMessage</code>","text":"<pre><code>message = UserMessage.new(\n \"What's my order status?\",\n session_id: \"session_123\",\n system_prompt: \"Be concise\",\n metadata: { source: \"web\" }\n)\n</code></pre> <p>Note: <code>UserMessage</code> is a standalone class, not a subclass of <code>Message</code>.</p>"},{"location":"api/messages/user-message/#constructor","title":"Constructor","text":"<pre><code>UserMessage.new(content, session_id: nil, system_prompt: nil, metadata: nil, id: nil)\n</code></pre> <p>Parameters:</p> Name Type Description <code>content</code> <code>String</code> Message text <code>session_id</code> <code>String</code>, <code>nil</code> Conversation session ID <code>system_prompt</code> <code>String</code>, <code>nil</code> Override system prompt <code>metadata</code> <code>Hash</code>, <code>nil</code> Additional metadata <code>id</code> <code>String</code>, <code>nil</code> Unique message ID (defaults to UUID)"},{"location":"api/messages/user-message/#attributes","title":"Attributes","text":""},{"location":"api/messages/user-message/#content","title":"content","text":"<pre><code>message.content # =&gt; String\n</code></pre> <p>The message text.</p>"},{"location":"api/messages/user-message/#session_id","title":"session_id","text":"<pre><code>message.session_id # =&gt; String | nil\n</code></pre> <p>Conversation session identifier for history persistence.</p>"},{"location":"api/messages/user-message/#system_prompt","title":"system_prompt","text":"<pre><code>message.system_prompt # =&gt; String | nil\n</code></pre> <p>Optional system prompt override for this message.</p>"},{"location":"api/messages/user-message/#metadata","title":"metadata","text":"<pre><code>message.metadata # =&gt; Hash\n</code></pre> <p>Arbitrary metadata (source, timestamp, user info, etc.).</p>"},{"location":"api/messages/user-message/#id","title":"id","text":"<pre><code>message.id # =&gt; String (UUID)\n</code></pre> <p>Unique message identifier.</p>"},{"location":"api/messages/user-message/#created_at","title":"created_at","text":"<pre><code>message.created_at # =&gt; Time\n</code></pre> <p>Message creation timestamp.</p>"},{"location":"api/messages/user-message/#methods","title":"Methods","text":""},{"location":"api/messages/user-message/#to_h","title":"to_h","text":"<pre><code>message.to_h # =&gt; Hash\n</code></pre> <p>Hash representation.</p> <p>Returns:</p> <pre><code>{\n content: \"What's my order status?\",\n session_id: \"session_123\",\n system_prompt: \"Be concise\",\n metadata: { source: \"web\" },\n id: \"uuid-here\",\n created_at: \"2024-01-15T10:30:00Z\"\n}\n</code></pre>"},{"location":"api/messages/user-message/#to_json","title":"to_json","text":"<pre><code>message.to_json # =&gt; String\n</code></pre> <p>JSON representation.</p>"},{"location":"api/messages/user-message/#to_message","title":"to_message","text":"<pre><code>message.to_message # =&gt; TextMessage\n</code></pre> <p>Converts to a <code>TextMessage</code> with role <code>\"user\"</code> for use in conversation history.</p>"},{"location":"api/messages/user-message/#to_s","title":"to_s","text":"<pre><code>message.to_s # =&gt; String\n</code></pre> <p>Returns the content string.</p>"},{"location":"api/messages/user-message/#selffrom","title":"self.from","text":"<pre><code>UserMessage.from(input) # =&gt; UserMessage\n</code></pre> <p>Creates a <code>UserMessage</code> from a String, Hash, or existing <code>UserMessage</code>.</p>"},{"location":"api/messages/user-message/#examples","title":"Examples","text":""},{"location":"api/messages/user-message/#basic-message","title":"Basic Message","text":"<pre><code>message = UserMessage.new(\"Hello!\")\n</code></pre>"},{"location":"api/messages/user-message/#with-session-id","title":"With Session ID","text":"<pre><code>message = UserMessage.new(\n \"Continue our conversation\",\n session_id: \"session_abc123\"\n)\n</code></pre>"},{"location":"api/messages/user-message/#with-system-prompt-override","title":"With System Prompt Override","text":"<pre><code>message = UserMessage.new(\n \"Translate this\",\n system_prompt: \"You are a translator. Respond in Spanish.\"\n)\n</code></pre>"},{"location":"api/messages/user-message/#with-metadata","title":"With Metadata","text":"<pre><code>message = UserMessage.new(\n \"Help with my account\",\n metadata: {\n source: \"mobile_app\",\n user_id: \"user_123\",\n session_id: \"sess_456\",\n locale: \"en-US\"\n }\n)\n</code></pre>"},{"location":"api/messages/user-message/#creating-from-various-inputs","title":"Creating from Various Inputs","text":"<pre><code># From a string\nmsg = UserMessage.from(\"Hello!\")\n\n# From a hash\nmsg = UserMessage.from(content: \"Hello!\", session_id: \"123\")\n\n# From an existing UserMessage (returns as-is)\nmsg = UserMessage.from(existing_message)\n</code></pre>"},{"location":"api/messages/user-message/#see-also","title":"See Also","text":"<ul> <li>Memory</li> <li>TextMessage</li> </ul>"},{"location":"api/streaming/","title":"Streaming","text":"<p>Real-time event streaming during robot and network execution.</p>"},{"location":"api/streaming/#overview","title":"Overview","text":"<p>The streaming system provides structured event publishing during LLM execution. Events are emitted for run lifecycle, content deltas (token streaming), tool calls, and metadata updates. The system supports nested contexts for network-level orchestration where multiple robots execute within a single run.</p> <pre><code>publish = -&gt;(event) {\n case event[:event]\n when \"text.delta\"\n print event[:data][:delta]\n when \"run.completed\"\n puts \"\\nDone!\"\n end\n}\n\ncontext = RobotLab::Streaming::Context.new(\n run_id: SecureRandom.uuid,\n message_id: SecureRandom.uuid,\n scope: \"robot\",\n publish: publish\n)\n\ncontext.publish_event(event: \"text.delta\", data: { delta: \"Hello\" })\n</code></pre>"},{"location":"api/streaming/#components","title":"Components","text":"Component Description Context Manages streaming state, sequencing, and event publishing Events Event type constants and classification helpers <p>Also used internally:</p> Component Description <code>SequenceCounter</code> Thread-safe monotonic counter for event ordering"},{"location":"api/streaming/#event-categories","title":"Event Categories","text":"Category Events Description Lifecycle <code>run.started</code>, <code>run.completed</code>, <code>run.failed</code>, <code>run.interrupted</code> Run-level state changes Steps <code>step.started</code>, <code>step.completed</code>, <code>step.failed</code> Durable execution steps Parts <code>part.created</code>, <code>part.completed</code>, <code>part.failed</code> Message composition parts Deltas <code>text.delta</code>, <code>tool_call.arguments.delta</code>, <code>reasoning.delta</code>, <code>data.delta</code> Token-level content streaming HITL <code>hitl.requested</code>, <code>hitl.resolved</code> Human-in-the-loop events Metadata <code>usage.updated</code>, <code>metadata.updated</code> Token usage and metadata Terminal <code>stream.ended</code> End of stream signal"},{"location":"api/streaming/#event-structure","title":"Event Structure","text":"<p>Each published event is a hash with the following shape:</p> <pre><code>{\n event: \"text.delta\", # Event type string\n data: { # Event payload (merged with context info)\n delta: \"Hello\", # Custom data\n run_id: \"run_123\", # Injected by context\n message_id: \"msg_456\", # Injected by context\n scope: \"robot\" # Injected by context\n },\n timestamp: 1707900000000, # Millisecond Unix timestamp\n sequence_number: 1, # Monotonically increasing\n id: \"publish-1:text.delta\" # Unique event ID\n}\n</code></pre>"},{"location":"api/streaming/#quick-start","title":"Quick Start","text":""},{"location":"api/streaming/#publishing-events","title":"Publishing Events","text":"<pre><code>context = RobotLab::Streaming::Context.new(\n run_id: \"run_123\",\n message_id: \"msg_456\",\n scope: \"network\",\n publish: -&gt;(event) { broadcast(event) }\n)\n\ncontext.publish_event(event: \"run.started\", data: { robot_name: \"assistant\" })\ncontext.publish_event(event: \"text.delta\", data: { delta: \"Hello \" })\ncontext.publish_event(event: \"text.delta\", data: { delta: \"world!\" })\ncontext.publish_event(event: \"run.completed\", data: {})\n</code></pre>"},{"location":"api/streaming/#nested-contexts-for-networks","title":"Nested Contexts for Networks","text":"<pre><code># Parent context for the network run\nnetwork_context = RobotLab::Streaming::Context.new(\n run_id: \"network_run_1\",\n message_id: \"msg_1\",\n scope: \"network\",\n publish: -&gt;(event) { stream_to_client(event) }\n)\n\n# Child context for each robot (shares the sequence counter)\nrobot_context = network_context.create_child_context(\"robot_run_1\")\nrobot_context.publish_event(event: \"text.delta\", data: { delta: \"Response\" })\n</code></pre>"},{"location":"api/streaming/#see-also","title":"See Also","text":"<ul> <li>Context</li> <li>Events</li> </ul>"},{"location":"api/streaming/context/","title":"Streaming::Context","text":"<p>Manages streaming event publishing with automatic sequencing, timestamping, and ID generation.</p>"},{"location":"api/streaming/context/#class-robotlabstreamingcontext","title":"Class: <code>RobotLab::Streaming::Context</code>","text":"<pre><code>context = RobotLab::Streaming::Context.new(\n run_id: \"run_123\",\n message_id: \"msg_456\",\n scope: \"network\",\n publish: -&gt;(event) { broadcast(event) }\n)\n\ncontext.publish_event(event: \"text.delta\", data: { delta: \"Hello\" })\n</code></pre>"},{"location":"api/streaming/context/#constructor","title":"Constructor","text":"<pre><code>Context.new(\n run_id:,\n message_id:,\n scope:,\n publish:,\n parent_run_id: nil,\n sequence_counter: nil\n)\n</code></pre> <p>Parameters:</p> Name Type Default Description <code>run_id</code> <code>String</code> required Unique identifier for this run <code>message_id</code> <code>String</code> required Current message identifier <code>scope</code> <code>String</code>, <code>Symbol</code> required Context scope (e.g., <code>\"network\"</code>, <code>\"robot\"</code>) <code>publish</code> <code>Proc</code> required Callback invoked with each event hash <code>parent_run_id</code> <code>String</code>, <code>nil</code> <code>nil</code> Parent run identifier for nested contexts <code>sequence_counter</code> <code>SequenceCounter</code>, <code>nil</code> <code>nil</code> Shared sequence counter (creates new one if nil)"},{"location":"api/streaming/context/#attributes","title":"Attributes","text":""},{"location":"api/streaming/context/#run_id","title":"run_id","text":"<pre><code>context.run_id # =&gt; String\n</code></pre> <p>The unique run identifier for this context.</p>"},{"location":"api/streaming/context/#parent_run_id","title":"parent_run_id","text":"<pre><code>context.parent_run_id # =&gt; String | nil\n</code></pre> <p>The parent run identifier. Set when this is a child context created by <code>create_child_context</code>.</p>"},{"location":"api/streaming/context/#message_id","title":"message_id","text":"<pre><code>context.message_id # =&gt; String\n</code></pre> <p>The current message identifier.</p>"},{"location":"api/streaming/context/#scope","title":"scope","text":"<pre><code>context.scope # =&gt; String\n</code></pre> <p>The context scope (converted to string). Typically <code>\"network\"</code> or <code>\"robot\"</code>.</p>"},{"location":"api/streaming/context/#methods","title":"Methods","text":""},{"location":"api/streaming/context/#publish_event","title":"publish_event","text":"<pre><code>chunk = context.publish_event(event:, data: {})\n</code></pre> <p>Publish a streaming event. The event is wrapped in a chunk with automatic sequencing, timestamping, and context metadata injection.</p> <p>Parameters:</p> Name Type Description <code>event</code> <code>String</code> Event type (e.g., <code>\"text.delta\"</code>, <code>\"run.started\"</code>) <code>data</code> <code>Hash</code> Event payload (default: <code>{}</code>) <p>Returns: The constructed event chunk hash.</p> <p>The chunk structure:</p> <pre><code>{\n event: \"text.delta\",\n data: {\n delta: \"Hello\", # from data parameter\n run_id: \"run_123\", # injected from context\n message_id: \"msg_456\", # injected from context\n scope: \"robot\" # injected from context\n },\n timestamp: 1707900000000, # millisecond Unix timestamp\n sequence_number: 1, # monotonically increasing\n id: \"publish-1:text.delta\" # unique event ID\n}\n</code></pre> <p>If the publish callback raises an error, it is caught and logged as a warning via <code>RobotLab.config.logger</code>. The chunk is still returned.</p>"},{"location":"api/streaming/context/#create_child_context","title":"create_child_context","text":"<pre><code>child = context.create_child_context(robot_run_id)\n</code></pre> <p>Create a child context for a nested robot execution. The child shares the same publish callback and sequence counter, ensuring events are ordered globally across the parent and child.</p> <p>Parameters:</p> Name Type Description <code>robot_run_id</code> <code>String</code> Run ID for the child context <p>Returns: A new <code>Context</code> with: - <code>run_id</code> set to <code>robot_run_id</code> - <code>parent_run_id</code> set to the current context's <code>run_id</code> - <code>scope</code> set to <code>\"robot\"</code> - A new <code>message_id</code> (generated UUID) - Shared <code>sequence_counter</code> and <code>publish</code> callback</p>"},{"location":"api/streaming/context/#create_context_with_shared_sequence","title":"create_context_with_shared_sequence","text":"<pre><code>sibling = context.create_context_with_shared_sequence(\n run_id: \"run_789\",\n message_id: \"msg_789\",\n scope: \"robot\"\n)\n</code></pre> <p>Create a new context that shares the same sequence counter as this context, but with different identifiers.</p> <p>Parameters:</p> Name Type Description <code>run_id</code> <code>String</code> Run ID for the new context <code>message_id</code> <code>String</code> Message ID for the new context <code>scope</code> <code>String</code> Scope for the new context <p>Returns: A new <code>Context</code> sharing the sequence counter and publish callback.</p>"},{"location":"api/streaming/context/#generate_part_id","title":"generate_part_id","text":"<pre><code>context.generate_part_id # =&gt; \"part_run_1234_900123_a1b2c3d4\"\n</code></pre> <p>Generate an OpenAI-compatible part ID (max 40 characters). Combines a truncated message ID, a timestamp suffix, and random hex.</p>"},{"location":"api/streaming/context/#generate_step_id","title":"generate_step_id","text":"<pre><code>context.generate_step_id(\"text_output\") # =&gt; \"publish-3:text_output\"\n</code></pre> <p>Generate a step ID for durable execution compatibility. Uses the current sequence number.</p> <p>Parameters:</p> Name Type Description <code>base_name</code> <code>String</code> Base name for the step"},{"location":"api/streaming/context/#generate_message_id","title":"generate_message_id","text":"<pre><code>context.generate_message_id # =&gt; \"a1b2c3d4-...\"\n</code></pre> <p>Generate a new UUID for use as a message identifier.</p>"},{"location":"api/streaming/context/#examples","title":"Examples","text":""},{"location":"api/streaming/context/#basic-event-publishing","title":"Basic Event Publishing","text":"<pre><code>publish = -&gt;(event) {\n puts \"[#{event[:event]}] #{event[:data]}\"\n}\n\ncontext = RobotLab::Streaming::Context.new(\n run_id: SecureRandom.uuid,\n message_id: SecureRandom.uuid,\n scope: \"robot\",\n publish: publish\n)\n\ncontext.publish_event(event: \"run.started\", data: { robot_name: \"assistant\" })\ncontext.publish_event(event: \"text.delta\", data: { delta: \"Hello \" })\ncontext.publish_event(event: \"text.delta\", data: { delta: \"world!\" })\ncontext.publish_event(event: \"run.completed\", data: {})\n</code></pre>"},{"location":"api/streaming/context/#network-with-child-contexts","title":"Network with Child Contexts","text":"<pre><code># Network-level context\nnetwork_ctx = RobotLab::Streaming::Context.new(\n run_id: \"net_run_1\",\n message_id: \"net_msg_1\",\n scope: \"network\",\n publish: -&gt;(e) { stream_to_client(e) }\n)\n\nnetwork_ctx.publish_event(event: \"run.started\", data: {})\n\n# Robot 1 executes\nrobot1_ctx = network_ctx.create_child_context(\"robot1_run_1\")\nrobot1_ctx.publish_event(event: \"step.started\", data: { robot: \"classifier\" })\nrobot1_ctx.publish_event(event: \"text.delta\", data: { delta: \"Category: billing\" })\nrobot1_ctx.publish_event(event: \"step.completed\", data: { robot: \"classifier\" })\n\n# Robot 2 executes (sequence numbers continue from robot 1)\nrobot2_ctx = network_ctx.create_child_context(\"robot2_run_1\")\nrobot2_ctx.publish_event(event: \"step.started\", data: { robot: \"responder\" })\nrobot2_ctx.publish_event(event: \"text.delta\", data: { delta: \"I can help with that.\" })\nrobot2_ctx.publish_event(event: \"step.completed\", data: { robot: \"responder\" })\n\nnetwork_ctx.publish_event(event: \"run.completed\", data: {})\n</code></pre>"},{"location":"api/streaming/context/#error-safe-publishing","title":"Error-Safe Publishing","text":"<pre><code># Errors in the publish callback are caught and logged,\n# so streaming failures do not interrupt execution.\ncontext = RobotLab::Streaming::Context.new(\n run_id: \"run_1\",\n message_id: \"msg_1\",\n scope: \"robot\",\n publish: -&gt;(e) { raise \"connection lost\" }\n)\n\n# This does not raise -- the error is logged via RobotLab.config.logger\nchunk = context.publish_event(event: \"text.delta\", data: { delta: \"test\" })\n# chunk is still returned with the event data\n</code></pre>"},{"location":"api/streaming/context/#see-also","title":"See Also","text":"<ul> <li>Streaming Overview</li> <li>Events</li> </ul>"},{"location":"api/streaming/events/","title":"Streaming::Events","text":"<p>Event type constants and classification helpers for the streaming system.</p>"},{"location":"api/streaming/events/#module-robotlabstreamingevents","title":"Module: <code>RobotLab::Streaming::Events</code>","text":"<p>Defines all recognized event types as string constants and provides helper methods for classifying events.</p> <pre><code>RobotLab::Streaming::Events::TEXT_DELTA # =&gt; \"text.delta\"\nRobotLab::Streaming::Events::RUN_COMPLETED # =&gt; \"run.completed\"\nRobotLab::Streaming::Events.delta?(\"text.delta\") # =&gt; true\n</code></pre>"},{"location":"api/streaming/events/#event-type-constants","title":"Event Type Constants","text":""},{"location":"api/streaming/events/#run-lifecycle-events","title":"Run Lifecycle Events","text":"Constant Value Description <code>RUN_STARTED</code> <code>\"run.started\"</code> A run has begun <code>RUN_COMPLETED</code> <code>\"run.completed\"</code> A run completed successfully <code>RUN_FAILED</code> <code>\"run.failed\"</code> A run failed with an error <code>RUN_INTERRUPTED</code> <code>\"run.interrupted\"</code> A run was interrupted"},{"location":"api/streaming/events/#step-events","title":"Step Events","text":"<p>For durable execution tracking:</p> Constant Value Description <code>STEP_STARTED</code> <code>\"step.started\"</code> A processing step has begun <code>STEP_COMPLETED</code> <code>\"step.completed\"</code> A processing step completed <code>STEP_FAILED</code> <code>\"step.failed\"</code> A processing step failed"},{"location":"api/streaming/events/#part-events","title":"Part Events","text":"<p>For message composition tracking:</p> Constant Value Description <code>PART_CREATED</code> <code>\"part.created\"</code> A message part was created <code>PART_COMPLETED</code> <code>\"part.completed\"</code> A message part completed <code>PART_FAILED</code> <code>\"part.failed\"</code> A message part failed"},{"location":"api/streaming/events/#content-delta-events","title":"Content Delta Events","text":"<p>Token-level streaming events:</p> Constant Value Description <code>TEXT_DELTA</code> <code>\"text.delta\"</code> A chunk of text content was generated <code>TOOL_CALL_ARGUMENTS_DELTA</code> <code>\"tool_call.arguments.delta\"</code> A chunk of tool call arguments <code>TOOL_CALL_OUTPUT_DELTA</code> <code>\"tool_call.output.delta\"</code> A chunk of tool call output <code>REASONING_DELTA</code> <code>\"reasoning.delta\"</code> A chunk of reasoning/thinking content <code>DATA_DELTA</code> <code>\"data.delta\"</code> A chunk of structured data"},{"location":"api/streaming/events/#human-in-the-loop-events","title":"Human-in-the-Loop Events","text":"Constant Value Description <code>HITL_REQUESTED</code> <code>\"hitl.requested\"</code> Human input has been requested <code>HITL_RESOLVED</code> <code>\"hitl.resolved\"</code> Human input has been provided"},{"location":"api/streaming/events/#metadata-events","title":"Metadata Events","text":"Constant Value Description <code>USAGE_UPDATED</code> <code>\"usage.updated\"</code> Token usage statistics updated <code>METADATA_UPDATED</code> <code>\"metadata.updated\"</code> Run metadata updated"},{"location":"api/streaming/events/#terminal-event","title":"Terminal Event","text":"Constant Value Description <code>STREAM_ENDED</code> <code>\"stream.ended\"</code> The stream has ended; no more events"},{"location":"api/streaming/events/#event-collections","title":"Event Collections","text":""},{"location":"api/streaming/events/#all_events","title":"ALL_EVENTS","text":"<pre><code>RobotLab::Streaming::Events::ALL_EVENTS\n# =&gt; Array of all event type strings (frozen)\n</code></pre>"},{"location":"api/streaming/events/#lifecycle_events","title":"LIFECYCLE_EVENTS","text":"<pre><code>RobotLab::Streaming::Events::LIFECYCLE_EVENTS\n# =&gt; [\"run.started\", \"run.completed\", \"run.failed\", \"run.interrupted\"]\n</code></pre>"},{"location":"api/streaming/events/#delta_events","title":"DELTA_EVENTS","text":"<pre><code>RobotLab::Streaming::Events::DELTA_EVENTS\n# =&gt; [\"text.delta\", \"tool_call.arguments.delta\", \"tool_call.output.delta\",\n# \"reasoning.delta\", \"data.delta\"]\n</code></pre>"},{"location":"api/streaming/events/#classification-methods","title":"Classification Methods","text":""},{"location":"api/streaming/events/#eventslifecycle","title":"Events.lifecycle?","text":"<pre><code>RobotLab::Streaming::Events.lifecycle?(\"run.started\") # =&gt; true\nRobotLab::Streaming::Events.lifecycle?(\"text.delta\") # =&gt; false\n</code></pre> <p>Returns <code>true</code> if the event is a run lifecycle event.</p>"},{"location":"api/streaming/events/#eventsdelta","title":"Events.delta?","text":"<pre><code>RobotLab::Streaming::Events.delta?(\"text.delta\") # =&gt; true\nRobotLab::Streaming::Events.delta?(\"run.completed\") # =&gt; false\n</code></pre> <p>Returns <code>true</code> if the event is a content delta (token streaming) event.</p>"},{"location":"api/streaming/events/#eventsvalid","title":"Events.valid?","text":"<pre><code>RobotLab::Streaming::Events.valid?(\"text.delta\") # =&gt; true\nRobotLab::Streaming::Events.valid?(\"unknown.event\") # =&gt; false\n</code></pre> <p>Returns <code>true</code> if the event is a recognized event type.</p>"},{"location":"api/streaming/events/#examples","title":"Examples","text":""},{"location":"api/streaming/events/#filtering-events-by-category","title":"Filtering Events by Category","text":"<pre><code>publish = -&gt;(event) {\n event_type = event[:event]\n\n if RobotLab::Streaming::Events.delta?(event_type)\n # Handle streaming content\n print event[:data][:delta]\n elsif RobotLab::Streaming::Events.lifecycle?(event_type)\n # Handle lifecycle transitions\n puts \"\\n[#{event_type}] run_id=#{event[:data][:run_id]}\"\n end\n}\n</code></pre>"},{"location":"api/streaming/events/#using-constants-for-event-matching","title":"Using Constants for Event Matching","text":"<pre><code>include RobotLab::Streaming::Events\n\npublish = -&gt;(event) {\n case event[:event]\n when TEXT_DELTA\n print event[:data][:delta]\n when TOOL_CALL_ARGUMENTS_DELTA\n buffer_tool_args(event[:data])\n when RUN_COMPLETED\n puts \"\\nRun complete\"\n when RUN_FAILED\n puts \"\\nRun failed: #{event[:data][:error]}\"\n when STREAM_ENDED\n cleanup\n end\n}\n</code></pre>"},{"location":"api/streaming/events/#validating-custom-events","title":"Validating Custom Events","text":"<pre><code>def emit(event_type, data)\n unless RobotLab::Streaming::Events.valid?(event_type)\n raise ArgumentError, \"Unknown event type: #{event_type}\"\n end\n\n context.publish_event(event: event_type, data: data)\nend\n</code></pre>"},{"location":"api/streaming/events/#see-also","title":"See Also","text":"<ul> <li>Streaming Overview</li> <li>Context</li> </ul>"},{"location":"architecture/","title":"Architecture Overview","text":"<p>RobotLab is designed around a few core architectural principles that enable flexible, composable AI workflows.</p>"},{"location":"architecture/#design-philosophy","title":"Design Philosophy","text":""},{"location":"architecture/#1-separation-of-concerns","title":"1. Separation of Concerns","text":"<p>Each component has a single, well-defined responsibility:</p> <ul> <li>Robot: LLM-powered agent (subclass of <code>RubyLLM::Agent</code>) with personality, tools, and memory</li> <li>Network: Orchestrates robot execution as a DAG pipeline via SimpleFlow</li> <li>Memory: Reactive key-value store for robot and network data</li> <li>Tool: Provides external capabilities to robots (inherits from <code>RubyLLM::Tool</code>)</li> <li>Task: Wraps a robot for pipeline execution with per-task configuration</li> </ul>"},{"location":"architecture/#2-composability","title":"2. Composability","text":"<p>Components are designed to be mixed and matched:</p> <ul> <li>Robots can be used standalone or within networks</li> <li>Tools can be shared across robots or scoped per-robot via <code>local_tools:</code></li> <li>Networks define DAG pipelines with sequential, parallel, and optional execution</li> <li>Memory can be standalone (per-robot) or shared (per-network)</li> <li><code>with_*</code> methods return <code>self</code> for fluent chaining</li> </ul>"},{"location":"architecture/#3-provider-agnostic","title":"3. Provider Agnostic","text":"<p>RobotLab abstracts away LLM provider differences through RubyLLM:</p> <ul> <li>Unified interface across Anthropic, OpenAI, Gemini, DeepSeek, Mistral, and others</li> <li>Consistent tool calling interface</li> <li>Automatic provider detection from model names</li> <li>Easy switching between providers via configuration</li> </ul>"},{"location":"architecture/#system-architecture","title":"System Architecture","text":"<pre><code>graph TB\n subgraph \"Application Layer\"\n A[Your Application]\n end\n\n subgraph \"RobotLab Core\"\n B[Network]\n C[Task]\n D[Robot &lt; RubyLLM::Agent]\n E[Memory]\n F[RobotResult]\n end\n\n subgraph \"Configuration\"\n G[Config &lt; MywayConfig::Base]\n end\n\n subgraph \"Integration Layer\"\n H[MCP Client]\n I[Tools &lt; RubyLLM::Tool]\n J[Templates / prompt_manager]\n end\n\n subgraph \"Execution Layer\"\n K[SimpleFlow::Pipeline]\n L[RubyLLM Chat]\n end\n\n subgraph \"Provider Layer\"\n M[Anthropic]\n N[OpenAI]\n O[Gemini]\n P[MCP Servers]\n end\n\n A --&gt; B\n A --&gt; D\n B --&gt; C\n C --&gt; D\n B --&gt; K\n B --&gt; E\n D --&gt; E\n D --&gt; L\n D --&gt; H\n D --&gt; I\n D --&gt; J\n D --&gt; F\n G --&gt; D\n G --&gt; L\n L --&gt; M\n L --&gt; N\n L --&gt; O\n H --&gt; P</code></pre>"},{"location":"architecture/#core-components","title":"Core Components","text":"Component Description Documentation Robot LLM agent (subclass of <code>RubyLLM::Agent</code>) with template-based prompts, tools, and memory Core Concepts Network Orchestrates multiple robots as a SimpleFlow pipeline Network Orchestration Memory Reactive key-value store with pub/sub and blocking reads Memory Management Task Wraps a robot for pipeline execution with per-task config Network Orchestration RobotResult Captures LLM output, tool calls, and metadata from a run Message Flow Config MywayConfig-based configuration with env var and file support Configuration"},{"location":"architecture/#configuration","title":"Configuration","text":"<p>RobotLab uses MywayConfig (<code>Config &lt; MywayConfig::Base</code>) instead of a <code>configure</code> block. Configuration is loaded from multiple sources in priority order:</p> <ol> <li>Bundled defaults (<code>lib/robot_lab/config/defaults.yml</code>)</li> <li>Environment overrides (development, test, production sections)</li> <li>XDG user config (<code>~/.config/robot_lab/config.yml</code>)</li> <li>Project config (<code>./config/robot_lab.yml</code>)</li> <li>Environment variables (<code>ROBOT_LAB_*</code> prefix, double underscore for nesting)</li> </ol> <pre><code># Access configuration\nRobotLab.config.ruby_llm.model #=&gt; \"claude-sonnet-4\"\nRobotLab.config.ruby_llm.request_timeout #=&gt; 120\n</code></pre>"},{"location":"architecture/#data-flow","title":"Data Flow","text":"<ol> <li>Input: User calls <code>robot.run(\"message\")</code> or <code>network.run(message: \"...\")</code></li> <li>Memory: Robot resolves active memory (standalone or network-shared)</li> <li>MCP: Robot resolves and initializes MCP clients from hierarchical config</li> <li>Tools: Robot resolves and filters tools from hierarchical config</li> <li>Execution: Robot delegates to <code>Agent#ask</code> which calls <code>@chat.ask</code> on RubyLLM</li> <li>Tool Loop: LLM may invoke tools; RubyLLM handles the tool call/result loop</li> <li>Result: Robot builds and returns a <code>RobotResult</code></li> <li>Network: If in a network, result flows to dependent tasks via SimpleFlow</li> </ol>"},{"location":"architecture/#key-patterns","title":"Key Patterns","text":""},{"location":"architecture/#factory-methods","title":"Factory Methods","text":"<p>Robots and networks are created via factory methods on the <code>RobotLab</code> module:</p> <pre><code>robot = RobotLab.build(\n name: \"assistant\",\n template: :assistant,\n context: { tone: \"friendly\" }\n)\n\nnetwork = RobotLab.create_network(name: \"pipeline\") do\n task :analyst, analyst_robot, depends_on: :none\n task :writer, writer_robot, depends_on: [:analyst]\nend\n</code></pre>"},{"location":"architecture/#fluent-chaining","title":"Fluent Chaining","text":"<p><code>with_*</code> methods on Robot delegate to the underlying <code>@chat</code> and return <code>self</code>:</p> <pre><code>robot = RobotLab.build(name: \"bot\")\n .with_instructions(\"Be concise.\")\n .with_temperature(0.3)\n .with_model(\"gpt-4o\")\n</code></pre>"},{"location":"architecture/#hierarchical-configuration","title":"Hierarchical Configuration","text":"<p>Tools and MCP servers use hierarchical resolution: <code>runtime &gt; robot build &gt; network &gt; global config</code>. Values can be <code>:none</code>, <code>:inherit</code>, or explicit arrays.</p>"},{"location":"architecture/#simpleflow-pipeline","title":"SimpleFlow Pipeline","text":"<p>Networks are thin wrappers around <code>SimpleFlow::Pipeline</code>. Each robot is wrapped in a <code>Task</code> that implements the <code>call(result)</code> interface. Tasks define dependencies (<code>:none</code>, <code>[:task_names]</code>, or <code>:optional</code>) to control execution order.</p>"},{"location":"architecture/#next-steps","title":"Next Steps","text":"<ul> <li>Core Concepts - Deep dive into robots and tools</li> <li>Robot Execution - How robots process messages</li> <li>Network Orchestration - Multi-robot workflows</li> <li>Memory Management - Managing memory and reactive features</li> <li>Message Flow - How messages move through the system</li> </ul>"},{"location":"architecture/core-concepts/","title":"Core Concepts","text":"<p>This page provides an in-depth look at RobotLab's fundamental building blocks.</p>"},{"location":"architecture/core-concepts/#robot","title":"Robot","text":"<p>A Robot is the primary unit of computation in RobotLab. It is a subclass of <code>RubyLLM::Agent</code> that wraps a persistent <code>@chat</code> with:</p> <ul> <li>A unique identity (name, description)</li> <li>A personality (system prompt and/or template)</li> <li>Capabilities (tools, MCP connections)</li> <li>Model and inference configuration</li> <li>Inherent memory (key-value store)</li> </ul>"},{"location":"architecture/core-concepts/#robot-anatomy","title":"Robot Anatomy","text":"<pre><code>robot = RobotLab.build(\n name: \"support_agent\", # Unique identifier\n description: \"Handles support requests\", # Used for routing hints\n model: \"claude-sonnet-4\", # LLM model\n system_prompt: &lt;&lt;~PROMPT, # Inline system prompt\n You are a friendly customer support agent for Acme Corp.\n Always be polite and helpful. If you don't know something,\n say so honestly.\n PROMPT\n local_tools: [OrderLookup, RefundProcessor], # RubyLLM::Tool subclasses\n mcp: :inherit, # Use network's MCP servers\n temperature: 0.7 # Inference parameter\n)\n</code></pre> <p>Or with a template:</p> <pre><code>robot = RobotLab.build(\n name: \"support_agent\",\n template: :support, # Loads prompts/support.md\n context: { company: \"Acme Corp\" }, # Template variables\n local_tools: [OrderLookup]\n)\n</code></pre>"},{"location":"architecture/core-concepts/#robot-lifecycle","title":"Robot Lifecycle","text":"<pre><code>stateDiagram-v2\n [*] --&gt; Created: RobotLab.build / Robot.new\n Created --&gt; Running: robot.run(\"message\")\n Running --&gt; ToolLoop: tool_call from LLM\n ToolLoop --&gt; Running: tool result sent back\n Running --&gt; Completed: final text response\n Completed --&gt; Running: robot.run(\"next message\")\n Completed --&gt; [*]: robot.disconnect</code></pre> <p>The persistent <code>@chat</code> maintains conversation history across multiple <code>run</code> calls, making the robot stateful.</p>"},{"location":"architecture/core-concepts/#robot-properties","title":"Robot Properties","text":"Property Type Description <code>name</code> <code>String</code> Unique identifier within network <code>description</code> <code>String</code>, <code>nil</code> What the robot does <code>model</code> <code>String</code> LLM model ID (resolved from chat) <code>template</code> <code>Symbol</code>, <code>nil</code> Prompt template identifier <code>system_prompt</code> <code>String</code>, <code>nil</code> Inline system prompt <code>local_tools</code> <code>Array</code> Locally defined tools <code>mcp_clients</code> <code>Hash</code> Connected MCP clients by server name <code>mcp_tools</code> <code>Array</code> Tools discovered from MCP servers <code>memory</code> <code>Memory</code> Inherent key-value memory <code>bus</code> <code>TypedBus::MessageBus</code>, <code>nil</code> Message bus instance <code>outbox</code> <code>Hash</code> Sent messages tracked with status and replies <code>mcp_config</code> <code>Symbol</code>, <code>Array</code> Build-time MCP configuration <code>tools_config</code> <code>Symbol</code>, <code>Array</code> Build-time tools configuration"},{"location":"architecture/core-concepts/#running-a-robot","title":"Running a Robot","text":"<p>The primary method is <code>robot.run(\"message\")</code>:</p> <pre><code>result = robot.run(\"What is the weather in Berlin?\")\nputs result.last_text_content\n</code></pre> <p>With runtime overrides:</p> <pre><code>result = robot.run(\"Analyze this\",\n memory: { data: report },\n mcp: :none,\n tools: :none\n)\n</code></pre> <p>With streaming:</p> <pre><code>robot.run(\"Tell me a story\") do |event|\n print event.text if event.respond_to?(:text)\nend\n</code></pre>"},{"location":"architecture/core-concepts/#tool","title":"Tool","text":"<p>Tools give robots the ability to interact with external systems. There are two patterns for defining tools.</p>"},{"location":"architecture/core-concepts/#rubyllmtool-subclass-primary","title":"RubyLLM::Tool Subclass (Primary)","text":"<pre><code>class GetWeather &lt; RubyLLM::Tool\n description \"Get current weather for a location\"\n\n param :location, type: \"string\", desc: \"City name\"\n param :unit, type: \"string\", desc: \"celsius or fahrenheit\"\n\n def execute(location:, unit: \"celsius\")\n WeatherAPI.current(location, unit: unit)\n end\nend\n</code></pre> <p>Tools defined as <code>RubyLLM::Tool</code> subclasses are passed to robots via <code>local_tools:</code>:</p> <pre><code>robot = RobotLab.build(\n name: \"weather_bot\",\n system_prompt: \"You provide weather information.\",\n local_tools: [GetWeather]\n)\n</code></pre>"},{"location":"architecture/core-concepts/#robotlabtoolcreate-factory","title":"RobotLab::Tool.create Factory","text":"<p>For simpler tools that do not need a class:</p> <pre><code>tool = RobotLab::Tool.create(\n name: \"get_time\",\n description: \"Get the current time\"\n) { |_args| Time.now.to_s }\n</code></pre> <p>With parameter schema:</p> <pre><code>tool = RobotLab::Tool.create(\n name: \"get_weather\",\n description: \"Get weather for a location\",\n parameters: {\n type: \"object\",\n properties: {\n location: { type: \"string\", description: \"City name\" }\n },\n required: [\"location\"]\n }\n) { |args| WeatherAPI.current(args[:location]) }\n</code></pre>"},{"location":"architecture/core-concepts/#tool-execution","title":"Tool Execution","text":"<p>When an LLM decides to use a tool:</p> <ol> <li>LLM generates a tool call with tool name and arguments</li> <li><code>@chat</code> (RubyLLM) identifies the tool from its registered tools</li> <li>Calls the <code>execute</code> method with keyword arguments</li> <li>Result is sent back to the LLM for continued processing</li> <li>Loop repeats until the LLM produces a final text response</li> </ol>"},{"location":"architecture/core-concepts/#error-handling","title":"Error Handling","text":"<p>Tool errors are captured and returned to the LLM:</p> <pre><code>def execute(order_id:)\n order = ORDERS[order_id]\n if order\n order\n else\n { error: \"Order not found\" }\n end\nend\n</code></pre>"},{"location":"architecture/core-concepts/#memory","title":"Memory","text":"<p>Memory is a reactive key-value store used by robots and networks.</p>"},{"location":"architecture/core-concepts/#standalone-vs-network-memory","title":"Standalone vs Network Memory","text":"<ul> <li>Standalone: Each robot has its own inherent <code>Memory</code> instance (<code>robot.memory</code>)</li> <li>In a Network: All robots share the network's <code>Memory</code> instance</li> </ul> <pre><code># Standalone memory\nrobot.memory[:user_id] = 123\nrobot.memory[:user_id] # =&gt; 123\n\n# Network memory is passed automatically\nnetwork = RobotLab.create_network(name: \"pipeline\") do\n task :robot_a, robot_a, depends_on: :none\n task :robot_b, robot_b, depends_on: [:robot_a]\nend\n# Both robot_a and robot_b share network.memory during execution\n</code></pre>"},{"location":"architecture/core-concepts/#reserved-keys","title":"Reserved Keys","text":"Key Type Description <code>:data</code> <code>Hash</code> Runtime data (accessible via <code>memory.data.key_name</code>) <code>:results</code> <code>Array</code> Accumulated robot results <code>:messages</code> <code>Array</code> Conversation history <code>:session_id</code> <code>String</code> Session identifier <code>:cache</code> <code>Module</code> Semantic cache (RubyLLM::SemanticCache)"},{"location":"architecture/core-concepts/#reactive-features","title":"Reactive Features","text":"<p>Memory supports pub/sub semantics for inter-robot communication:</p> <pre><code># Write a value (notifies subscribers, wakes waiters)\nmemory.set(:sentiment, { score: 0.8 })\n\n# Read a value (non-blocking)\nmemory.get(:sentiment) # =&gt; { score: 0.8 } or nil\n\n# Blocking read (waits until value exists)\nmemory.get(:sentiment, wait: true) # Blocks indefinitely\nmemory.get(:sentiment, wait: 30) # Blocks up to 30 seconds\n\n# Subscribe to changes\nmemory.subscribe(:sentiment) do |change|\n puts \"#{change.key} = #{change.value} (written by #{change.writer})\"\nend\n</code></pre>"},{"location":"architecture/core-concepts/#message-types","title":"Message Types","text":"<p>RobotLab uses a type hierarchy for messages:</p> <pre><code>classDiagram\n Message &lt;|-- TextMessage\n Message &lt;|-- ToolCallMessage\n Message &lt;|-- ToolResultMessage\n\n class Message {\n +String type\n +String role\n +content\n +String stop_reason\n +text?()\n +tool_call?()\n +tool_result?()\n +stopped?()\n }\n\n class TextMessage {\n +String content\n }\n\n class ToolCallMessage {\n +Array~ToolMessage~ tools\n }\n\n class ToolResultMessage {\n +ToolMessage tool\n +Hash content\n +success?()\n +error?()\n }</code></pre>"},{"location":"architecture/core-concepts/#message-roles","title":"Message Roles","text":"Role Description <code>user</code> Input from the user <code>assistant</code> Response from the LLM <code>system</code> System instructions <code>tool_result</code> Tool execution result"},{"location":"architecture/core-concepts/#stop-reasons","title":"Stop Reasons","text":"Reason Description <code>stop</code> Natural completion <code>tool</code> Tool call requested"},{"location":"architecture/core-concepts/#robotresult","title":"RobotResult","text":"<p>The output from a robot execution:</p> <pre><code>result = robot.run(\"Hello!\")\n\nresult.robot_name # =&gt; \"support_agent\"\nresult.output # =&gt; [TextMessage, ...]\nresult.tool_calls # =&gt; [ToolResultMessage, ...]\nresult.stop_reason # =&gt; \"stop\"\nresult.created_at # =&gt; Time\nresult.id # =&gt; UUID string\n</code></pre>"},{"location":"architecture/core-concepts/#accessing-response-content","title":"Accessing Response Content","text":"<pre><code># Get last text response (most common)\ntext = result.last_text_content\n\n# Check if tools were called\nhas_tools = result.has_tool_calls?\n\n# Check if execution completed naturally\nresult.stopped?\n\n# Serialization\nresult.export # =&gt; Hash (excludes debug fields)\nresult.to_h # =&gt; Hash (includes debug fields)\nresult.to_json # =&gt; JSON string\n</code></pre>"},{"location":"architecture/core-concepts/#configuration","title":"Configuration","text":"<p>RobotLab uses <code>MywayConfig</code> for configuration. There is no <code>RobotLab.configure</code> block. Configuration is loaded from:</p> <ol> <li>Bundled defaults (<code>lib/robot_lab/config/defaults.yml</code>)</li> <li>Environment-specific overrides</li> <li>XDG config files (<code>~/.config/robot_lab/config.yml</code>)</li> <li>Project config (<code>./config/robot_lab.yml</code>)</li> <li>Environment variables (<code>ROBOT_LAB_*</code> prefix)</li> </ol> <p>Access via <code>RobotLab.config</code>:</p> <pre><code>RobotLab.config.ruby_llm.model # =&gt; \"claude-sonnet-4\"\nRobotLab.config.ruby_llm.request_timeout # =&gt; 120\n</code></pre>"},{"location":"architecture/core-concepts/#configuration-hierarchy","title":"Configuration Hierarchy","text":"<p>Tools and MCP servers use a cascading configuration system:</p> <pre><code>RobotLab.config (global)\n|\n+-- mcp: [server1, server2]\n+-- tools: [tool1, tool2]\n|\n+-- Network\n| |\n| +-- mcp: :inherit | :none | [servers]\n| +-- tools: :inherit | :none | [tools]\n| |\n| +-- Task (per-step config)\n| | +-- context: { department: \"billing\" }\n| | +-- mcp: :none | :inherit | [servers]\n| | +-- tools: :none | :inherit | [tools]\n| |\n| +-- Robot (build-time config)\n| |\n| +-- mcp: :inherit | :none | [servers]\n| +-- tools: :inherit | :none | [tools]\n| |\n| +-- run() call (runtime config)\n| +-- mcp: :none | [servers]\n| +-- tools: :none | [tools]\n</code></pre> <p>Resolution order: runtime &gt; robot build-time &gt; task &gt; network &gt; global config.</p> <p>The <code>:inherit</code> value pulls from the parent level. <code>:none</code> explicitly disables.</p>"},{"location":"architecture/core-concepts/#message-bus","title":"Message Bus","text":"<p>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.</p>"},{"location":"architecture/core-concepts/#how-it-works","title":"How It Works","text":"<p>Robots connect to a shared <code>TypedBus::MessageBus</code> via the <code>bus:</code> parameter. Each robot gets a typed channel (accepting only <code>RobotMessage</code> objects) named after its <code>name</code>. Messages are delivered asynchronously via the <code>async</code> gem's fiber scheduler.</p> <pre><code>bus = TypedBus::MessageBus.new\n\nbob = RobotLab.build(name: \"bob\", system_prompt: \"You tell jokes.\", bus: bus)\nalice = RobotLab.build(name: \"alice\", system_prompt: \"You evaluate jokes.\", bus: bus)\n</code></pre>"},{"location":"architecture/core-concepts/#robotmessage","title":"RobotMessage","text":"<p><code>RobotMessage</code> is an immutable <code>Data.define</code> value object used as the typed envelope:</p> Field Type Description <code>id</code> <code>Integer</code> Per-robot sequential counter <code>from</code> <code>String</code> Sender's robot name (= channel name) <code>content</code> <code>String</code>, <code>Hash</code> Message payload <code>in_reply_to</code> <code>String</code>, <code>nil</code> Composite key of the original message (e.g., <code>\"alice:1\"</code>) <p>Methods: <code>key</code> returns <code>\"from:id\"</code> composite identity; <code>reply?</code> returns true when <code>in_reply_to</code> is set.</p>"},{"location":"architecture/core-concepts/#sending-and-receiving","title":"Sending and Receiving","text":"<pre><code># Send a message to another robot\nalice.send_message(to: :bob, content: \"Tell me a joke.\")\n\n# Handle incoming messages with auto-ack (1 arg)\nbob.on_message do |message|\n joke = bob.run(message.content.to_s).last_text_content\n bob.send_reply(to: message.from.to_sym, content: joke, in_reply_to: message.key)\nend\n</code></pre> <p>Block arity controls delivery handling: 1 argument auto-acks; 2 arguments give manual control over <code>delivery.ack!</code>/<code>delivery.nack!</code>.</p>"},{"location":"architecture/core-concepts/#dynamic-spawning","title":"Dynamic Spawning","text":"<p>Robots can create new robots at runtime using <code>spawn</code>. The bus is created lazily \u2014 no upfront wiring required:</p> <pre><code>dispatcher = RobotLab.build(name: \"dispatcher\", system_prompt: \"You delegate work.\")\n\n# spawn creates a child on the same bus (bus created automatically)\nhelper = dispatcher.spawn(name: \"helper\", system_prompt: \"You answer questions.\")\n\n# The child can immediately communicate with the parent\nanswer = helper.run(\"What is 2+2?\").last_text_content\nhelper.send_message(to: :dispatcher, content: answer)\n</code></pre> <p>Robots can also join a bus after creation using <code>with_bus</code>:</p> <pre><code>bot = RobotLab.build(name: \"late_joiner\", system_prompt: \"Hello.\")\nbot.with_bus(existing_bus) # now connected to the bus\n</code></pre> <p>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:</p> <pre><code>worker1 = dispatcher.spawn(name: \"worker\", system_prompt: \"Worker 1\")\nworker2 = dispatcher.spawn(name: \"worker\", system_prompt: \"Worker 2\")\ndispatcher.send_message(to: :worker, content: \"Do this task\")\n# Both worker1 and worker2 receive the message\n</code></pre>"},{"location":"architecture/core-concepts/#bus-vs-network","title":"Bus vs Network","text":"Feature Network Message Bus Execution model DAG (acyclic) Cyclic, bidirectional Communication Sequential pipeline Pub/sub channels Memory Shared network memory Independent per-robot Use case Linear workflows Negotiation, convergence <p>The bus is purely additive \u2014 robots without <code>bus:</code> work exactly as before.</p>"},{"location":"architecture/core-concepts/#network","title":"Network","text":"<p>A Network orchestrates multiple robots in a pipeline workflow using SimpleFlow:</p> <pre><code>network = RobotLab.create_network(name: \"support\") do\n task :classifier, classifier_robot, depends_on: :none\n task :billing, billing_robot, depends_on: :optional\n task :technical, technical_robot, depends_on: :optional\nend\n\nresult = network.run(message: \"I need help with billing\")\n</code></pre> <p>Networks provide:</p> <ul> <li>DAG-based execution via SimpleFlow with <code>depends_on:</code> for sequencing</li> <li>Parallel execution for tasks with the same dependencies</li> <li>Optional tasks activated dynamically by classifier robots</li> <li>Shared memory for inter-robot communication</li> <li>Per-task configuration via the <code>Task</code> wrapper</li> <li>Broadcast messaging for network-wide announcements</li> </ul>"},{"location":"architecture/core-concepts/#next-steps","title":"Next Steps","text":"<ul> <li>Robot Execution - Detailed execution flow</li> <li>Network Orchestration - Multi-robot coordination</li> <li>Using Tools - Creating and using tools</li> </ul>"},{"location":"architecture/message-flow/","title":"Message Flow","text":"<p>This page explains how messages move through RobotLab, from user input to LLM response.</p>"},{"location":"architecture/message-flow/#message-types","title":"Message Types","text":"<p>RobotLab uses four primary message types:</p> <pre><code>classDiagram\n class Message {\n &lt;&lt;abstract&gt;&gt;\n +type: String\n +role: String\n +content: String\n +stop_reason: String\n }\n\n class TextMessage {\n +text?() bool\n +user?() bool\n +assistant?() bool\n +system?() bool\n }\n\n class ToolMessage {\n +id: String\n +name: String\n +input: Hash\n +tool_call?() bool\n }\n\n class ToolCallMessage {\n +tools: Array~ToolMessage~\n }\n\n class ToolResultMessage {\n +tool: ToolMessage\n +content: Hash\n +tool_result?() bool\n }\n\n Message &lt;|-- TextMessage\n Message &lt;|-- ToolCallMessage\n Message &lt;|-- ToolResultMessage\n ToolMessage -- ToolCallMessage\n ToolMessage -- ToolResultMessage</code></pre>"},{"location":"architecture/message-flow/#textmessage","title":"TextMessage","text":"<p>Regular text content from users or assistants:</p> <pre><code>TextMessage.new(\n role: \"user\",\n content: \"What's the weather in Paris?\"\n)\n\nTextMessage.new(\n role: \"assistant\",\n content: \"The weather in Paris is sunny and 22 degrees C.\",\n stop_reason: \"stop\"\n)\n</code></pre>"},{"location":"architecture/message-flow/#toolmessage","title":"ToolMessage","text":"<p>Represents a tool invocation with its parameters:</p> <pre><code>ToolMessage.new(\n id: \"tool_123\",\n name: \"get_weather\",\n input: { location: \"Paris\" }\n)\n</code></pre>"},{"location":"architecture/message-flow/#toolcallmessage","title":"ToolCallMessage","text":"<p>LLM's request to execute one or more tools:</p> <pre><code>ToolCallMessage.new(\n role: \"assistant\",\n content: nil,\n stop_reason: \"tool\",\n tools: [\n ToolMessage.new(id: \"call_1\", name: \"get_weather\", input: { location: \"Paris\" })\n ]\n)\n</code></pre>"},{"location":"architecture/message-flow/#toolresultmessage","title":"ToolResultMessage","text":"<p>Result from tool execution:</p> <pre><code>ToolResultMessage.new(\n tool: tool_message,\n content: { data: { temp: 22, condition: \"sunny\" } }\n)\n</code></pre>"},{"location":"architecture/message-flow/#message-flow-standalone-robot","title":"Message Flow: Standalone Robot","text":"<p>The primary execution path is <code>robot.run(\"message\")</code>:</p> <pre><code>sequenceDiagram\n participant User\n participant Robot\n participant Memory\n participant MCP\n participant Tools\n participant Agent\n participant Chat\n participant LLM\n\n User-&gt;&gt;Robot: robot.run(\"message\")\n Robot-&gt;&gt;Memory: resolve_active_memory\n Memory--&gt;&gt;Robot: active memory\n\n Robot-&gt;&gt;MCP: resolve_mcp_hierarchy\n MCP--&gt;&gt;Robot: resolved MCP config\n Robot-&gt;&gt;Robot: ensure_mcp_clients\n\n Robot-&gt;&gt;Tools: resolve_tools_hierarchy\n Tools--&gt;&gt;Robot: filtered tools\n Robot-&gt;&gt;Chat: @chat.with_tools(...)\n\n Robot-&gt;&gt;Agent: ask(\"message\")\n Agent-&gt;&gt;Chat: @chat.ask(\"message\")\n Chat-&gt;&gt;LLM: Provider API call\n\n loop Tool Loop (handled by RubyLLM)\n LLM--&gt;&gt;Chat: Tool call response\n Chat-&gt;&gt;Tools: Execute tool\n Tools--&gt;&gt;Chat: Tool result\n Chat-&gt;&gt;LLM: Send tool result\n end\n\n LLM--&gt;&gt;Chat: Final response\n Chat--&gt;&gt;Agent: RubyLLM::Response\n Agent--&gt;&gt;Robot: response\n\n Robot-&gt;&gt;Robot: build_result(response, memory)\n Robot--&gt;&gt;User: RobotResult</code></pre>"},{"location":"architecture/message-flow/#step-by-step","title":"Step-by-Step","text":"<ol> <li> <p><code>robot.run(\"message\")</code>: Entry point. Accepts a positional string argument.</p> </li> <li> <p>Resolve Memory: Determines which memory to use:</p> </li> <li><code>network_memory</code> if provided (network execution)</li> <li><code>network.memory</code> if in a network context</li> <li> <p><code>robot.memory</code> (standalone, the default)</p> </li> <li> <p>Merge Runtime Memory: If a <code>memory:</code> keyword argument is passed, it is merged into the active memory.</p> </li> <li> <p>Set Current Writer: Sets <code>memory.current_writer = robot.name</code> so subscription callbacks know which robot wrote a value.</p> </li> <li> <p>Resolve MCP Hierarchy: Resolves MCP server configuration through the hierarchy: <code>runtime &gt; robot build &gt; network &gt; global config</code>.</p> </li> <li> <p>Ensure MCP Clients: Initializes or updates MCP client connections. Discovers tools from connected MCP servers.</p> </li> <li> <p>Resolve Tools Hierarchy: Resolves which tools are available through the same hierarchy.</p> </li> <li> <p>Filter Tools: Applies the resolved tool list to <code>@chat.with_tools(...)</code>.</p> </li> <li> <p>Agent#ask: Delegates to the parent class <code>RubyLLM::Agent#ask</code>, which calls <code>@chat.ask(message)</code>.</p> </li> <li> <p>LLM Interaction: RubyLLM handles the provider-specific API call, including the tool call/result loop.</p> </li> <li> <p>Build Result: Wraps the LLM response in a <code>RobotResult</code> containing output messages, tool calls, and metadata.</p> </li> <li> <p>Return: Returns the <code>RobotResult</code> to the caller.</p> </li> </ol>"},{"location":"architecture/message-flow/#message-flow-network-execution","title":"Message Flow: Network Execution","text":"<p>When running through a network, the flow adds pipeline orchestration:</p> <pre><code>sequenceDiagram\n participant User\n participant Network\n participant Pipeline\n participant Task\n participant Robot\n participant LLM\n\n User-&gt;&gt;Network: network.run(message: \"...\")\n Network-&gt;&gt;Network: Inject network_memory into run_context\n Network-&gt;&gt;Pipeline: SimpleFlow::Pipeline.call_parallel(initial_result)\n\n loop For each ready task\n Pipeline-&gt;&gt;Task: task.call(result)\n Task-&gt;&gt;Task: Deep merge task context with run_params\n Task-&gt;&gt;Robot: robot.call(enhanced_result)\n Robot-&gt;&gt;Robot: extract_run_context(result)\n Robot-&gt;&gt;Robot: run(message, network_memory: ...)\n Robot-&gt;&gt;LLM: Agent#ask -&gt; @chat.ask\n LLM--&gt;&gt;Robot: Response\n Robot--&gt;&gt;Task: result.with_context(:name, robot_result).continue(robot_result)\n Task--&gt;&gt;Pipeline: SimpleFlow::Result\n end\n\n Pipeline--&gt;&gt;Network: Final SimpleFlow::Result\n Network--&gt;&gt;User: result</code></pre>"},{"location":"architecture/message-flow/#key-points","title":"Key Points","text":"<ul> <li>Network creates initial result: <code>SimpleFlow::Result.new(run_context, context: { run_params: run_context })</code></li> <li>Task wraps robot: Each <code>Task</code> deep-merges its own context with the run params before delegating to the robot</li> <li>Robot extracts context: <code>extract_run_context(result)</code> pulls the message, MCP, tools, and memory from the SimpleFlow result</li> <li>Shared memory: All robots use <code>network.memory</code> during network execution</li> <li>Result accumulation: Each task stores its <code>RobotResult</code> in <code>result.context[:task_name]</code></li> </ul>"},{"location":"architecture/message-flow/#robotresult","title":"RobotResult","text":"<p>The return value of <code>robot.run(\"message\")</code>:</p> <pre><code>result = robot.run(\"What is Ruby?\")\n\nresult.last_text_content #=&gt; \"Ruby is a dynamic programming language...\"\nresult.has_tool_calls? #=&gt; false\nresult.robot_name #=&gt; \"assistant\"\nresult.output #=&gt; [TextMessage(role: \"assistant\", content: \"...\")]\nresult.tool_calls #=&gt; []\nresult.stop_reason #=&gt; \"stop\"\nresult.created_at #=&gt; Time\nresult.id #=&gt; \"uuid\"\nresult.checksum #=&gt; \"sha256-hex\"\n</code></pre>"},{"location":"architecture/message-flow/#result-serialization","title":"Result Serialization","text":"<pre><code># Export for persistence (excludes debug fields)\nhash = result.export\n\n# Full hash including debug fields\nhash = result.to_h\n\n# JSON\njson = result.to_json\n\n# Reconstruct from hash\nresult = RobotResult.from_hash(hash)\n</code></pre>"},{"location":"architecture/message-flow/#message-predicates","title":"Message Predicates","text":"<p>Check message types:</p> <pre><code>message.text? # Is it a TextMessage?\nmessage.tool_call? # Is it a ToolCallMessage?\nmessage.tool_result? # Is it a ToolResultMessage?\n\nmessage.user? # Is role \"user\"?\nmessage.assistant? # Is role \"assistant\"?\nmessage.system? # Is role \"system\"?\n\nmessage.stopped? # Is stop_reason \"stop\"?\nmessage.tool_stop? # Is stop_reason \"tool\"?\n</code></pre>"},{"location":"architecture/message-flow/#creating-messages","title":"Creating Messages","text":""},{"location":"architecture/message-flow/#from-strings","title":"From Strings","text":"<pre><code>TextMessage.new(role: \"user\", content: \"Hello\")\n</code></pre>"},{"location":"architecture/message-flow/#from-hashes","title":"From Hashes","text":"<pre><code>Message.from_hash(\n type: \"text\",\n role: \"user\",\n content: \"Hello\"\n)\n</code></pre>"},{"location":"architecture/message-flow/#serialization","title":"Serialization","text":"<p>Messages can be serialized:</p> <pre><code># To hash\nhash = message.to_h\n#=&gt; { type: \"text\", role: \"user\", content: \"Hello\" }\n\n# To JSON\njson = message.to_json\n\n# From hash\nmessage = Message.from_hash(hash)\n</code></pre>"},{"location":"architecture/message-flow/#template-resolution","title":"Template Resolution","text":"<p>When a robot has a template, it is resolved at build time via prompt_manager:</p> <pre><code>robot = RobotLab.build(\n name: \"helper\",\n template: :helper,\n context: { tone: \"friendly\" }\n)\n</code></pre> <p>The template resolution process: 1. <code>PM.parse(:helper)</code> loads the template file from the configured prompts directory 2. YAML front matter is extracted and applied to the chat (model, temperature, etc.) 3. The template body is rendered with the provided context 4. The rendered text is set as system instructions via <code>@chat.with_instructions(rendered)</code></p> <p>If both <code>template:</code> and <code>system_prompt:</code> are provided, the template is applied first, then the system prompt is appended via a second <code>@chat.with_instructions</code> call.</p>"},{"location":"architecture/message-flow/#next-steps","title":"Next Steps","text":"<ul> <li>Memory Management - How memory stores conversation data</li> <li>Network Orchestration - Multi-robot pipeline execution</li> </ul>"},{"location":"architecture/network-orchestration/","title":"Network Orchestration","text":"<p>Networks coordinate multiple robots using SimpleFlow pipelines for DAG-based execution.</p>"},{"location":"architecture/network-orchestration/#network-structure","title":"Network Structure","text":"<p>A network is a thin wrapper around <code>SimpleFlow::Pipeline</code>:</p> <ul> <li>Pipeline: DAG-based execution engine</li> <li>Robots: Named collection of robot instances</li> <li>Tasks: Wrap robots with per-task configuration and define dependencies</li> <li>Memory: Shared reactive memory for inter-robot communication</li> </ul> <pre><code>network = RobotLab.create_network(name: \"customer_service\") do\n task :classifier, classifier_robot, depends_on: :none\n task :billing, billing_robot, depends_on: :optional\n task :technical, technical_robot, depends_on: :optional\nend\n</code></pre>"},{"location":"architecture/network-orchestration/#creating-networks","title":"Creating Networks","text":"<p>Networks are created via <code>RobotLab.create_network</code> with a block DSL:</p> <pre><code>analyst = RobotLab.build(name: \"analyst\", system_prompt: \"Analyze the input.\")\nwriter = RobotLab.build(name: \"writer\", system_prompt: \"Write a report.\")\nreviewer = RobotLab.build(name: \"reviewer\", system_prompt: \"Review the report.\")\n\nnetwork = RobotLab.create_network(name: \"pipeline\") do\n task :analyst, analyst, depends_on: :none\n task :writer, writer, depends_on: [:analyst]\n task :reviewer, reviewer, depends_on: [:writer]\nend\n\nresult = network.run(message: \"Analyze this quarterly data\")\n</code></pre>"},{"location":"architecture/network-orchestration/#task-configuration","title":"Task Configuration","text":"<p>Tasks can have per-task configuration that is deep-merged with network run params:</p> <pre><code>network = RobotLab.create_network(name: \"support\") do\n task :classifier, classifier_robot, depends_on: :none\n task :billing, billing_robot,\n context: { department: \"billing\", escalation_level: 2 },\n tools: [RefundTool],\n depends_on: :optional\n task :technical, technical_robot,\n context: { department: \"technical\" },\n mcp: [filesystem_server],\n depends_on: :optional\nend\n</code></pre>"},{"location":"architecture/network-orchestration/#task-parameters","title":"Task Parameters","text":"Parameter Type Description <code>name</code> Symbol Task/step name <code>robot</code> Robot The robot instance <code>context</code> Hash Task-specific context (deep-merged with run params) <code>mcp</code> Symbol, Array MCP server config (<code>:none</code>, <code>:inherit</code>, or array) <code>tools</code> Symbol, Array Tools config (<code>:none</code>, <code>:inherit</code>, or array) <code>memory</code> Memory, Hash, nil Task-specific memory <code>depends_on</code> Symbol, Array Dependencies (<code>:none</code>, <code>:optional</code>, or task names)"},{"location":"architecture/network-orchestration/#execution-model","title":"Execution Model","text":"<pre><code>stateDiagram-v2\n [*] --&gt; Start\n Start --&gt; ExecuteTask: next ready task\n ExecuteTask --&gt; CheckDependents: task complete\n CheckDependents --&gt; ExecuteTask: more tasks ready\n CheckDependents --&gt; Complete: all tasks done\n ExecuteTask --&gt; Halted: task halts\n Complete --&gt; [*]\n Halted --&gt; [*]</code></pre>"},{"location":"architecture/network-orchestration/#task-dependency-types","title":"Task Dependency Types","text":"Type Description <code>:none</code> No dependencies, runs first <code>[:task1, :task2]</code> Waits for listed tasks to complete <code>:optional</code> Only runs when explicitly activated"},{"location":"architecture/network-orchestration/#robotcall-interface","title":"Robot#call Interface","text":"<p>Each robot implements the SimpleFlow step interface via <code>call(result)</code>:</p> <pre><code># Inside Robot (simplified)\ndef call(result)\n run_context = extract_run_context(result)\n message = run_context.delete(:message)\n\n robot_result = run(message, **run_context)\n\n result\n .with_context(@name.to_sym, robot_result)\n .continue(robot_result)\nend\n</code></pre>"},{"location":"architecture/network-orchestration/#extract_run_context","title":"extract_run_context","text":"<p>The <code>extract_run_context</code> method pulls parameters from the SimpleFlow result:</p> <ul> <li>Extracts <code>:mcp</code>, <code>:tools</code>, <code>:memory</code>, and <code>:network_memory</code> from <code>run_params</code></li> <li>Merges the current result value into the context</li> <li>If the previous result value is a <code>RobotResult</code>, extracts its <code>last_text_content</code> as the message</li> <li>If it is a String, uses it directly as the message</li> <li>If it is a Hash, merges it with the run params</li> </ul>"},{"location":"architecture/network-orchestration/#taskcall-interface","title":"Task#call Interface","text":"<p>Each <code>Task</code> wraps a robot and enhances the SimpleFlow result before delegation:</p> <pre><code># Inside Task (simplified)\ndef call(result)\n # Deep merge task context with run_params\n run_params = deep_merge(\n result.context[:run_params] || {},\n @context\n )\n\n # Add task-specific config\n run_params[:mcp] = @mcp unless @mcp == :none\n run_params[:tools] = @tools unless @tools == :none\n run_params[:memory] = @memory if @memory\n\n enhanced_result = result.with_context(:run_params, run_params)\n @robot.call(enhanced_result)\nend\n</code></pre>"},{"location":"architecture/network-orchestration/#simpleflowresult","title":"SimpleFlow::Result","text":"<p>The result object flows through the pipeline:</p> <pre><code>result.value # Current task's output (RobotResult)\nresult.context # Accumulated context from all tasks\nresult.halted? # Whether execution stopped early\nresult.continued? # Whether execution continues\n</code></pre>"},{"location":"architecture/network-orchestration/#result-methods","title":"Result Methods","text":"Method Description <code>continue(value)</code> Continue to next tasks <code>halt(value)</code> Stop pipeline execution <code>with_context(key, val)</code> Add data to context <code>activate(task_name)</code> Enable an optional task"},{"location":"architecture/network-orchestration/#context-structure","title":"Context Structure","text":"<pre><code>{\n run_params: { message: \"...\", customer_id: 123, network_memory: memory },\n classifier: RobotResult, # Stored by Robot#call\n billing: RobotResult,\n # ... other task results\n}\n</code></pre>"},{"location":"architecture/network-orchestration/#optional-task-activation","title":"Optional Task Activation","text":"<p>Optional tasks (those with <code>depends_on: :optional</code>) do not run automatically. They must be activated by a preceding task using <code>result.activate(:task_name)</code>.</p> <p>This pattern is commonly used with a classifier robot that analyzes the input and routes to the appropriate handler:</p> <pre><code>classifier = RobotLab.build(\n name: \"classifier\",\n system_prompt: \"Classify the request. Respond with: BILLING, TECHNICAL, or GENERAL.\"\n)\n\nbilling = RobotLab.build(name: \"billing\", system_prompt: \"Handle billing requests.\")\ntechnical = RobotLab.build(name: \"technical\", system_prompt: \"Handle technical requests.\")\ngeneral = RobotLab.build(name: \"general\", system_prompt: \"Handle general requests.\")\n\nnetwork = RobotLab.create_network(name: \"support\") do\n task :classifier, classifier, depends_on: :none\n task :billing, billing, depends_on: :optional\n task :technical, technical, depends_on: :optional\n task :general, general, depends_on: :optional\nend\n</code></pre>"},{"location":"architecture/network-orchestration/#classifier-robot-pattern","title":"Classifier Robot Pattern","text":"<p>To activate optional tasks, a robot subclass overrides <code>call</code> to inspect its own output and activate the appropriate downstream task:</p> <pre><code>class ClassifierRobot &lt; RobotLab::Robot\n def call(result)\n run_context = extract_run_context(result)\n message = run_context.delete(:message)\n\n robot_result = run(message, **run_context)\n\n new_result = result\n .with_context(@name.to_sym, robot_result)\n .continue(robot_result)\n\n # Analyze output and activate the appropriate optional task\n category = robot_result.last_text_content.to_s.downcase\n\n case category\n when /billing/\n new_result.activate(:billing)\n when /technical/\n new_result.activate(:technical)\n else\n new_result.activate(:general)\n end\n end\nend\n</code></pre>"},{"location":"architecture/network-orchestration/#shared-memory","title":"Shared Memory","text":"<p>All robots in a network share the network's memory during execution. The network injects its memory into the run context:</p> <pre><code># Inside Network#run\ndef run(**run_context)\n run_context[:network_memory] = @memory\n initial_result = SimpleFlow::Result.new(\n run_context,\n context: { run_params: run_context }\n )\n @pipeline.call_parallel(initial_result)\nend\n</code></pre> <p>Robots use the shared memory for inter-robot communication:</p> <pre><code># Robot A writes to shared memory\nmemory.set(:classification, \"billing\")\n\n# Robot B reads from shared memory\ncategory = memory.get(:classification, wait: true)\n</code></pre> <p>See Memory Management for details on reactive features like subscriptions and blocking reads.</p>"},{"location":"architecture/network-orchestration/#broadcasting","title":"Broadcasting","text":"<p>Networks support a broadcast channel for network-wide announcements:</p> <pre><code># Register handlers\nnetwork.on_broadcast do |message|\n case message[:payload][:event]\n when :pause\n pause_current_work\n when :resume\n resume_work\n end\nend\n\n# Send broadcasts\nnetwork.broadcast(event: :pause, reason: \"rate limit hit\")\nnetwork.broadcast(event: :phase_complete, phase: \"analysis\")\n</code></pre> <p>Broadcasts are dispatched asynchronously and also written to memory at the <code>_network_broadcast</code> key, so robots can subscribe via <code>memory.subscribe(:_network_broadcast)</code>.</p>"},{"location":"architecture/network-orchestration/#parallel-execution","title":"Parallel Execution","text":"<p>Tasks with the same dependencies can run in parallel:</p> <pre><code>network = RobotLab.create_network(name: \"analysis\", concurrency: :threads) do\n task :fetch, fetcher, depends_on: :none\n\n # These three run in parallel after :fetch completes\n task :sentiment, sentiment_bot, depends_on: [:fetch]\n task :entities, entity_bot, depends_on: [:fetch]\n task :keywords, keyword_bot, depends_on: [:fetch]\n\n # Waits for all three\n task :merge, merger, depends_on: [:sentiment, :entities, :keywords]\nend\n</code></pre>"},{"location":"architecture/network-orchestration/#concurrency-modes","title":"Concurrency Modes","text":"Mode Description <code>:auto</code> SimpleFlow chooses best mode <code>:threads</code> Use Ruby threads <code>:async</code> Use async/fiber"},{"location":"architecture/network-orchestration/#data-flow","title":"Data Flow","text":"<ol> <li>Initial Value: <code>network.run(**params)</code> creates an initial <code>SimpleFlow::Result</code> with the run context</li> <li>Run Params: Stored in <code>result.context[:run_params]</code></li> <li>Task Results: Each task adds its <code>RobotResult</code> to context under its task name</li> <li>Final Value: Last task's output becomes <code>result.value</code></li> </ol> <pre><code>result = network.run(\n message: \"Help with billing\",\n customer_id: 123\n)\n\nresult.context[:run_params] #=&gt; { message: \"...\", customer_id: 123, network_memory: ... }\nresult.context[:classifier] #=&gt; RobotResult from classifier\nresult.context[:billing] #=&gt; RobotResult from billing robot\nresult.value #=&gt; Final RobotResult\n</code></pre>"},{"location":"architecture/network-orchestration/#visualization","title":"Visualization","text":"<p>Networks provide visualization methods via the underlying SimpleFlow pipeline:</p> <pre><code># ASCII representation\nputs network.visualize\n\n# Mermaid diagram\nputs network.to_mermaid\n\n# DOT format (Graphviz)\nputs network.to_dot\n\n# Execution plan description\nputs network.execution_plan\n</code></pre>"},{"location":"architecture/network-orchestration/#network-inspection","title":"Network Inspection","text":"<pre><code># Get a robot by name\nnetwork.robot(:classifier) #=&gt; Robot\nnetwork[:classifier] #=&gt; Robot (alias)\n\n# List all robots\nnetwork.available_robots #=&gt; [Robot, Robot, ...]\n\n# Add a robot without a task\nnetwork.add_robot(extra_robot)\n\n# Convert to hash\nnetwork.to_h\n#=&gt; { name: \"support\", robots: [\"classifier\", \"billing\"], tasks: [...], optional_tasks: [...] }\n</code></pre>"},{"location":"architecture/network-orchestration/#next-steps","title":"Next Steps","text":"<ul> <li>Memory Management - Shared memory and reactive features</li> <li>Message Flow - How messages are processed within robots</li> </ul>"},{"location":"architecture/robot-execution/","title":"Robot Execution","text":"<p>This page details how a robot processes messages and generates responses.</p>"},{"location":"architecture/robot-execution/#execution-overview","title":"Execution Overview","text":"<p>When you call <code>robot.run(\"message\")</code>, several steps occur:</p> <pre><code>sequenceDiagram\n participant App as Application\n participant Robot\n participant Memory\n participant Chat as @chat (RubyLLM)\n participant LLM\n\n App-&gt;&gt;Robot: run(\"message\")\n Robot-&gt;&gt;Memory: resolve_active_memory()\n Robot-&gt;&gt;Robot: resolve_mcp_hierarchy()\n Robot-&gt;&gt;Robot: resolve_tools_hierarchy()\n Robot-&gt;&gt;Robot: ensure_mcp_clients()\n Robot-&gt;&gt;Robot: filtered_tools()\n Robot-&gt;&gt;Chat: with_tools(*filtered)\n Robot-&gt;&gt;Chat: ask(\"message\")\n Chat-&gt;&gt;LLM: API Request\n\n loop Tool Calls\n LLM--&gt;&gt;Chat: tool_call response\n Chat-&gt;&gt;Chat: execute tool\n Chat-&gt;&gt;LLM: tool result\n end\n\n LLM--&gt;&gt;Chat: final response\n Chat--&gt;&gt;Robot: RubyLLM::Response\n Robot-&gt;&gt;Robot: build_result(response)\n Robot--&gt;&gt;App: RobotResult</code></pre>"},{"location":"architecture/robot-execution/#step-by-step-flow","title":"Step-by-Step Flow","text":""},{"location":"architecture/robot-execution/#1-memory-resolution","title":"1. Memory Resolution","text":"<p>The robot determines which memory to use for this run:</p> <pre><code># Priority order:\n# 1. Explicit network_memory: parameter\n# 2. Network's memory (if running in a network)\n# 3. Robot's inherent @memory (standalone mode)\nrun_memory = resolve_active_memory(network: network, network_memory: network_memory)\n\n# Merge runtime memory if provided\ncase memory\nwhen Memory then run_memory = memory\nwhen Hash then run_memory.merge!(memory)\nend\n\n# Track who is writing to memory\nrun_memory.current_writer = @name\n</code></pre>"},{"location":"architecture/robot-execution/#2-mcp-hierarchy-resolution","title":"2. MCP Hierarchy Resolution","text":"<p>MCP servers are resolved through a hierarchy: runtime &gt; robot build-time &gt; network &gt; global config.</p> <pre><code># Resolve build-time config against network/global\nparent_value = network&amp;.network&amp;.mcp || RobotLab.config.mcp\nbuild_resolved = ToolConfig.resolve_mcp(@mcp_config, parent_value: parent_value)\n\n# Then resolve runtime override against build-time\nresolved_mcp = ToolConfig.resolve_mcp(runtime_mcp, parent_value: build_resolved)\n</code></pre> <p>Values at each level:</p> <ul> <li><code>:none</code> -- no MCP servers at this level</li> <li><code>:inherit</code> -- use parent level's MCP config</li> <li><code>Array</code> -- explicit list of server configurations</li> </ul>"},{"location":"architecture/robot-execution/#3-mcp-client-initialization","title":"3. MCP Client Initialization","text":"<p>If MCP servers need to be connected (or reconnected), the robot initializes clients:</p> <pre><code># Connect to each MCP server\nmcp_servers.each do |server_config|\n client = MCP::Client.new(server_config)\n client.connect\n\n if client.connected?\n @mcp_clients[client.server.name] = client\n discover_mcp_tools(client, server_name) # Auto-discover tools\n end\nend\n</code></pre>"},{"location":"architecture/robot-execution/#4-tools-resolution","title":"4. Tools Resolution","text":"<p>Tools are resolved through the same hierarchy and filtered:</p> <pre><code># Collect all available tools\navailable = @local_tools + @mcp_tools\n\n# Apply whitelist if specified\nfiltered = ToolConfig.filter_tools(available, allowed_names: resolved_tools)\n\n# Apply tools to the persistent chat\n@chat.with_tools(*filtered) if filtered.any?\n</code></pre>"},{"location":"architecture/robot-execution/#5-llm-inference","title":"5. LLM Inference","text":"<p>The message is sent to the LLM via <code>Agent#ask</code>, which delegates to <code>@chat.ask</code>:</p> <pre><code># Robot#run calls Agent#ask\nresponse = ask(message, **kwargs)\n\n# Internally, Agent#ask calls:\n# @chat.ask(message)\n</code></pre> <p>The persistent <code>@chat</code> (a <code>RubyLLM::Chat</code> instance) handles:</p> <ul> <li>Maintaining conversation history</li> <li>Sending the system prompt</li> <li>Formatting messages for the provider</li> <li>Executing the tool call loop automatically</li> </ul>"},{"location":"architecture/robot-execution/#6-tool-execution-loop","title":"6. Tool Execution Loop","text":"<p>RubyLLM's <code>@chat</code> handles the tool loop automatically. When the LLM requests a tool call:</p> <ol> <li><code>@chat</code> identifies the tool from its registered tools</li> <li>Calls the tool's <code>execute</code> method (for <code>RubyLLM::Tool</code> subclasses) or <code>call</code> method (for <code>RobotLab::Tool</code>)</li> <li>Sends the result back to the LLM</li> <li>Repeats until the LLM produces a final text response</li> </ol> <p>The <code>on_tool_call</code> and <code>on_tool_result</code> callbacks fire during this loop if configured:</p> <pre><code># These callbacks are registered on @chat during Robot#initialize\n@chat.on_tool_call(&amp;@on_tool_call) if @on_tool_call\n@chat.on_tool_result(&amp;@on_tool_result) if @on_tool_result\n</code></pre>"},{"location":"architecture/robot-execution/#7-result-construction","title":"7. Result Construction","text":"<p>After the LLM responds, a <code>RobotResult</code> is built:</p> <pre><code>def build_result(response, _memory)\n output = if response.respond_to?(:content) &amp;&amp; response.content\n [TextMessage.new(role: 'assistant', content: response.content)]\n else\n []\n end\n\n tool_calls = response.respond_to?(:tool_calls) ? (response.tool_calls || []) : []\n\n RobotResult.new(\n robot_name: @name,\n output: output,\n tool_calls: normalize_tool_calls(tool_calls),\n stop_reason: response.respond_to?(:stop_reason) ? response.stop_reason : nil\n )\nend\n</code></pre>"},{"location":"architecture/robot-execution/#robotresult","title":"RobotResult","text":"<p>The result object from a <code>robot.run</code> call:</p> <pre><code>result = robot.run(\"Hello!\")\n\nresult.robot_name # =&gt; \"assistant\"\nresult.output # =&gt; [TextMessage, ...]\nresult.tool_calls # =&gt; [ToolResultMessage, ...]\nresult.stop_reason # =&gt; \"stop\" or nil\nresult.created_at # =&gt; Time\nresult.id # =&gt; UUID string\n\n# Convenience methods\nresult.last_text_content # =&gt; \"Hi there!\" (last text message content)\nresult.has_tool_calls? # =&gt; false\nresult.stopped? # =&gt; true\n</code></pre>"},{"location":"architecture/robot-execution/#streaming","title":"Streaming","text":"<p>Robots support streaming by passing a block to <code>run</code>:</p> <pre><code>result = robot.run(\"Tell me a story\") do |event|\n print event.text if event.respond_to?(:text)\nend\n</code></pre> <p>The block is forwarded to <code>Agent#ask</code> which passes it to <code>@chat.ask</code>. Streaming events are provider-specific but typically include text deltas.</p>"},{"location":"architecture/robot-execution/#template-resolution","title":"Template Resolution","text":"<p>When a robot has a <code>template:</code>, it is resolved during initialization:</p> <pre><code># 1. Parse the template via prompt_manager\nparsed = PM.parse(@template)\n\n# 2. Extract and apply front matter config\n# (model, temperature, top_p, etc.)\napply_front_matter_config(parsed.metadata)\n\n# 3. Render the template body with context\nrendered = parsed.to_s(**resolved_context)\n\n# 4. Set as system instructions on @chat\n@chat.with_instructions(rendered)\n</code></pre>"},{"location":"architecture/robot-execution/#front-matter-config-keys","title":"Front Matter Config Keys","text":"<p>Templates can configure the chat via YAML front matter:</p> Key Effect <code>model</code> Sets the LLM model <code>temperature</code> Sets randomness <code>top_p</code> Sets nucleus sampling <code>top_k</code> Sets top-k sampling <code>max_tokens</code> Sets max response tokens <code>presence_penalty</code> Sets presence penalty <code>frequency_penalty</code> Sets frequency penalty <code>stop</code> Sets stop sequences"},{"location":"architecture/robot-execution/#model-selection","title":"Model Selection","text":"<p>The model is determined by:</p> <ol> <li>Robot's explicit <code>model:</code> parameter</li> <li>Front matter <code>model</code> from template</li> <li>Global <code>RobotLab.config.ruby_llm.model</code></li> </ol> <pre><code>robot = RobotLab.build(\n name: \"bot\",\n model: \"claude-sonnet-4\" # Takes precedence\n)\n\n# Or configure globally via config files / environment variables\n# ROBOT_LAB_RUBY_LLM__MODEL=gpt-4o\n</code></pre>"},{"location":"architecture/robot-execution/#simpleflow-integration","title":"SimpleFlow Integration","text":"<p>When a robot runs inside a network, the <code>call</code> method is invoked by SimpleFlow:</p> <pre><code>sequenceDiagram\n participant SF as SimpleFlow\n participant Task as Task Wrapper\n participant Robot\n participant Chat as @chat\n\n SF-&gt;&gt;Task: call(result)\n Task-&gt;&gt;Task: deep_merge(run_params, task_context)\n Task-&gt;&gt;Robot: call(enhanced_result)\n Robot-&gt;&gt;Robot: extract_run_context(result)\n Robot-&gt;&gt;Robot: message = context.delete(:message)\n Robot-&gt;&gt;Robot: run(message, **context)\n Robot-&gt;&gt;Chat: ask(message)\n Chat--&gt;&gt;Robot: response\n Robot--&gt;&gt;SF: result.continue(robot_result)</code></pre> <p>The <code>Task</code> wrapper deep-merges per-task configuration (context, mcp, tools) before delegating to the robot's <code>call</code>. The base <code>Robot#call</code> extracts the message and calls <code>run</code>:</p> <pre><code>def call(result)\n run_context = extract_run_context(result)\n message = run_context.delete(:message)\n robot_result = run(message, **run_context)\n\n result\n .with_context(@name.to_sym, robot_result)\n .continue(robot_result)\nend\n</code></pre>"},{"location":"architecture/robot-execution/#next-steps","title":"Next Steps","text":"<ul> <li>Network Orchestration - Multi-robot coordination</li> <li>Core Concepts - Fundamental building blocks</li> <li>Using Tools - Creating and using tools</li> </ul>"},{"location":"architecture/state-management/","title":"Memory Management","text":"<p>Memory in RobotLab is a reactive key-value store that provides persistent storage for runtime data, conversation history, and arbitrary user-defined values. It replaces the old <code>State</code> class with a unified system that supports both standalone robot usage and shared network execution.</p>"},{"location":"architecture/state-management/#memory-structure","title":"Memory Structure","text":"<p>The <code>Memory</code> class holds:</p> <pre><code>memory = RobotLab.create_memory(data: { user_id: \"123\" })\n\nmemory.data # StateProxy - custom key-value data with method-style access\nmemory.results # Array&lt;RobotResult&gt; - execution history\nmemory.messages # Array&lt;Message&gt; - conversation history\nmemory.session_id # String - optional persistence identifier\nmemory.cache # RubyLLM::SemanticCache - semantic caching module\n</code></pre>"},{"location":"architecture/state-management/#standalone-robot-memory","title":"Standalone Robot Memory","text":"<p>Every robot has its own inherent memory instance, accessible via <code>robot.memory</code>:</p> <pre><code>robot = RobotLab.build(\n name: \"assistant\",\n system_prompt: \"You are helpful.\"\n)\n\n# Access the robot's memory\nrobot.memory[:user_name] = \"Alice\"\nrobot.memory[:user_name] #=&gt; \"Alice\"\n\n# Run the robot\nresult = robot.run(\"Hello!\")\n\n# Memory persists between runs on the same robot instance\nrobot.memory[:preference] = \"dark_mode\"\nresult2 = robot.run(\"What are my preferences?\")\n\n# Reset memory to initial state\nrobot.reset_memory\n</code></pre>"},{"location":"architecture/state-management/#network-shared-memory","title":"Network Shared Memory","text":"<p>When robots execute within a network, they share the network's memory instead of using their own inherent memory. This enables inter-robot communication.</p> <pre><code>classifier = RobotLab.build(name: \"classifier\", system_prompt: \"Classify requests.\")\nhandler = RobotLab.build(name: \"handler\", system_prompt: \"Handle requests.\")\n\nnetwork = RobotLab.create_network(name: \"support\") do\n task :classifier, classifier, depends_on: :none\n task :handler, handler, depends_on: [:classifier]\nend\n\n# The network has its own shared memory\nnetwork.memory[:customer_tier] = \"premium\"\n\n# All robots in the network read/write from network.memory during execution\nresult = network.run(message: \"I need help with billing\")\n\n# Reset network memory between runs if needed\nnetwork.reset_memory\n</code></pre> <p>The memory resolution logic is:</p> <ol> <li>If <code>network_memory</code> is provided at runtime, use that</li> <li>If the robot is in a network, use the network's shared memory</li> <li>Otherwise, use the robot's own inherent memory (<code>robot.memory</code>)</li> </ol>"},{"location":"architecture/state-management/#creating-memory","title":"Creating Memory","text":""},{"location":"architecture/state-management/#basic-creation","title":"Basic Creation","text":"<pre><code>memory = RobotLab.create_memory\n</code></pre>"},{"location":"architecture/state-management/#with-initial-data","title":"With Initial Data","text":"<pre><code>memory = RobotLab.create_memory(\n data: {\n user_id: \"user_123\",\n order_id: \"ord_456\",\n priority: \"high\"\n }\n)\n</code></pre>"},{"location":"architecture/state-management/#with-caching-disabled","title":"With Caching Disabled","text":"<pre><code>memory = RobotLab.create_memory(data: {}, enable_cache: false)\n</code></pre>"},{"location":"architecture/state-management/#reserved-keys","title":"Reserved Keys","text":"<p>Memory has five reserved keys with special behavior and dedicated accessors:</p> Key Type Description <code>:data</code> <code>StateProxy</code> Runtime data with method-style access <code>:results</code> <code>Array&lt;RobotResult&gt;</code> Accumulated robot execution results <code>:messages</code> <code>Array&lt;Message&gt;</code> Conversation history <code>:session_id</code> <code>String</code> Conversation session identifier <code>:cache</code> <code>RubyLLM::SemanticCache</code> Semantic cache module (read-only after init) <p>Reserved keys are accessed through dedicated methods and are excluded from <code>memory.keys</code>:</p> <pre><code>memory.data[:category] = \"billing\"\nmemory.data.category #=&gt; \"billing\" (method-style via StateProxy)\n\nmemory.results #=&gt; []\nmemory.session_id #=&gt; nil\nmemory.cache #=&gt; RubyLLM::SemanticCache\n</code></pre>"},{"location":"architecture/state-management/#stateproxy","title":"StateProxy","text":"<p>The <code>data</code> attribute is a <code>StateProxy</code> that provides convenient hash-style and method-style access:</p> <pre><code>memory.data[:user_id] # Hash-style access\nmemory.data[:user_id] = \"456\" # Assignment\n\nmemory.data.user_id # Method-style access\nmemory.data.user_id = \"456\" # Method-style assignment\n\nmemory.data.key?(:user_id) # Check existence\nmemory.data.keys # Get all keys\nmemory.data.to_h # Convert to plain hash\n</code></pre>"},{"location":"architecture/state-management/#reactive-features","title":"Reactive Features","text":"<p>Memory supports pub/sub semantics where robots can subscribe to key changes and optionally block until values become available.</p>"},{"location":"architecture/state-management/#setting-values","title":"Setting Values","text":"<p>Use <code>memory.set(key, value)</code> to write a value and notify subscribers asynchronously:</p> <pre><code>memory.set(:sentiment, { score: 0.8, confidence: 0.95 })\n</code></pre> <p>The <code>[]=</code> operator also triggers reactive notifications for non-reserved keys:</p> <pre><code>memory[:sentiment] = { score: 0.8 } # Equivalent to memory.set(:sentiment, ...)\n</code></pre>"},{"location":"architecture/state-management/#blocking-reads","title":"Blocking Reads","text":"<p>Use <code>memory.get(key, wait:)</code> to block until a value becomes available. This is useful for concurrent pipeline execution where one robot needs to wait for another's output:</p> <pre><code># Immediate read (returns nil if missing)\nmemory.get(:sentiment)\n\n# Block indefinitely until value exists\nmemory.get(:sentiment, wait: true)\n\n# Block up to 30 seconds, raise AwaitTimeout if exceeded\nmemory.get(:sentiment, wait: 30)\n\n# Wait for multiple keys at once\nresults = memory.get(:sentiment, :entities, :keywords, wait: 60)\n#=&gt; { sentiment: {...}, entities: [...], keywords: [...] }\n</code></pre>"},{"location":"architecture/state-management/#subscriptions","title":"Subscriptions","text":"<p>Subscribe to key changes with async callbacks. The callback receives a <code>MemoryChange</code> object:</p> <pre><code>memory.subscribe(:raw_data) do |change|\n puts \"#{change.key} changed from #{change.previous} to #{change.value}\"\n puts \"Written by: #{change.writer} at #{change.timestamp}\"\nend\n\n# Subscribe to multiple keys\nmemory.subscribe(:sentiment, :entities) do |change|\n update_dashboard(change.key, change.value)\nend\n\n# Pattern-based subscriptions (glob-style matching)\nmemory.subscribe_pattern(\"analysis:*\") do |change|\n puts \"Analysis key #{change.key} updated\"\nend\n\n# Unsubscribe\nsub_id = memory.subscribe(:status) { |c| puts c.value }\nmemory.unsubscribe(sub_id)\n\n# Check if key has subscribers\nmemory.subscribed?(:status) #=&gt; true/false\n</code></pre>"},{"location":"architecture/state-management/#memorychange","title":"MemoryChange","text":"<p>The <code>MemoryChange</code> object provides context about what changed:</p> <pre><code>change.key #=&gt; :sentiment\nchange.value #=&gt; { score: 0.8 }\nchange.previous #=&gt; nil (or previous value)\nchange.writer #=&gt; \"classifier\" (robot name)\nchange.network_name #=&gt; \"support_pipeline\"\nchange.timestamp #=&gt; Time\nchange.created? #=&gt; true (new key, no previous value)\nchange.updated? #=&gt; false\nchange.deleted? #=&gt; false\n</code></pre>"},{"location":"architecture/state-management/#memory-lifecycle","title":"Memory Lifecycle","text":""},{"location":"architecture/state-management/#results","title":"Results","text":"<p>Results track the history of robot executions:</p> <pre><code># Append a result\nmemory.append_result(robot_result)\n\n# Get all results (returns a copy)\nmemory.results\n\n# Get results from a specific index (for incremental persistence)\nmemory.results_from(5)\n</code></pre> <p>Each <code>RobotResult</code> contains:</p> <pre><code>result.robot_name # Which robot produced this\nresult.output # Array&lt;Message&gt; - response content\nresult.tool_calls # Array&lt;ToolResultMessage&gt; - tools called\nresult.stop_reason # Stop reason from LLM\nresult.last_text_content # Convenience: last text content string\nresult.has_tool_calls? # Whether any tools were called\nresult.created_at # When it was created\n</code></pre>"},{"location":"architecture/state-management/#format-history","title":"Format History","text":"<p>The <code>format_history</code> method prepares messages for LLM consumption:</p> <pre><code>formatted = memory.format_history\n# Returns combined messages + formatted results\n</code></pre>"},{"location":"architecture/state-management/#merge","title":"Merge","text":"<p>Merge additional values into memory:</p> <pre><code>memory.merge!(user_id: 123, category: \"billing\")\n</code></pre>"},{"location":"architecture/state-management/#key-management","title":"Key Management","text":"<pre><code>memory.key?(:user_id) # Check existence\nmemory.keys # Get all non-reserved keys\nmemory.all_keys # Get all keys including reserved\nmemory.delete(:temp_data) # Delete a specific key\nmemory.clear # Clear all non-reserved keys\nmemory.reset # Reset to initial state (preserves cache)\n</code></pre>"},{"location":"architecture/state-management/#cloning","title":"Cloning","text":"<p>Create independent copies of memory for isolated execution. Subscriptions are not cloned:</p> <pre><code>original = RobotLab.create_memory(data: { count: 1 })\ncloned = original.clone\n\ncloned[:count] = 2\noriginal[:count] #=&gt; still 1\n</code></pre>"},{"location":"architecture/state-management/#serialization","title":"Serialization","text":"<p>Convert memory to and from hash for persistence:</p> <pre><code># To hash\nhash = memory.to_h\n#=&gt; {\n# data: { ... },\n# results: [...],\n# messages: [...],\n# session_id: \"abc123\",\n# custom: { my_key: \"value\" }\n# }\n\n# To JSON\njson = memory.to_json\n\n# From hash\nmemory = Memory.from_hash(hash)\n</code></pre>"},{"location":"architecture/state-management/#semantic-cache","title":"Semantic Cache","text":"<p>Memory includes a semantic cache via <code>RubyLLM::SemanticCache</code> that reduces costs and latency by returning cached responses for semantically equivalent queries:</p> <pre><code># Using the cache with fetch\nresponse = memory.cache.fetch(\"What is Ruby?\") do\n RubyLLM.chat.ask(\"What is Ruby?\")\nend\n\n# Wrapping a chat instance\nchat = memory.cache.wrap(RubyLLM.chat(model: \"gpt-4o\"))\nchat.ask(\"What is Ruby?\") # Cached on semantic similarity\n</code></pre> <p>Caching can be disabled per-memory or per-robot:</p> <pre><code>memory = RobotLab.create_memory(enable_cache: false)\nrobot = RobotLab.build(name: \"bot\", system_prompt: \"...\", enable_cache: false)\n</code></pre>"},{"location":"architecture/state-management/#backend-options","title":"Backend Options","text":"<p>Memory defaults to a Hash-based backend but can use Redis for distributed scenarios:</p> <pre><code># Auto-detect (uses Redis if available, falls back to Hash)\nmemory = Memory.new(backend: :auto)\n\n# Force Hash backend\nmemory = Memory.new(backend: :hash)\n\n# Force Redis backend\nmemory = Memory.new(backend: :redis)\n\n# Check backend\nmemory.redis? #=&gt; true/false\n</code></pre> <p>Redis is configured via <code>RobotLab.config.redis</code> or the <code>REDIS_URL</code> environment variable.</p>"},{"location":"architecture/state-management/#best-practices","title":"Best Practices","text":""},{"location":"architecture/state-management/#1-use-memory-for-cross-robot-data","title":"1. Use Memory for Cross-Robot Data","text":"<pre><code># In a network, robots share memory automatically.\n# Robot A writes:\nmemory.set(:classification, \"billing\")\n\n# Robot B reads:\ncategory = memory.get(:classification)\n</code></pre>"},{"location":"architecture/state-management/#2-use-blocking-reads-for-concurrent-pipelines","title":"2. Use Blocking Reads for Concurrent Pipelines","text":"<pre><code># When robots run in parallel, use blocking reads\n# to synchronize on shared data:\nresults = memory.get(:sentiment, :entities, wait: 60)\n</code></pre>"},{"location":"architecture/state-management/#3-keep-data-minimal","title":"3. Keep Data Minimal","text":"<pre><code># Store references instead of large objects\nmemory[:response_id] = response.id # Preferred\n# memory[:huge_response] = api_response # Avoid\n</code></pre>"},{"location":"architecture/state-management/#4-reset-between-independent-runs","title":"4. Reset Between Independent Runs","text":"<pre><code>network.reset_memory\nresult = network.run(message: \"New conversation\")\n</code></pre>"},{"location":"architecture/state-management/#next-steps","title":"Next Steps","text":"<ul> <li>Network Orchestration - How networks share memory</li> <li>Message Flow - How messages are processed</li> </ul>"},{"location":"examples/","title":"Examples","text":"<p>Complete working examples demonstrating RobotLab features.</p>"},{"location":"examples/#overview","title":"Overview","text":"<p>These examples show how to use RobotLab for common scenarios, from simple chatbots to complex multi-robot systems.</p>"},{"location":"examples/#examples_1","title":"Examples","text":"Example Description Basic Chat Simple conversational robot Multi-Robot Network Customer service with routing Tool Usage External API integration MCP Server Creating an MCP tool server Rails Application Full Rails integration Message Bus Bidirectional robot communication with convergence Spawning Robots Dynamic specialist creation at runtime"},{"location":"examples/#quick-links","title":"Quick Links","text":""},{"location":"examples/#simple-examples","title":"Simple Examples","text":"<ul> <li>Hello World Robot</li> <li>Robot with Tools</li> <li>Network with Routing</li> </ul>"},{"location":"examples/#advanced-examples","title":"Advanced Examples","text":"<ul> <li>Streaming Responses</li> <li>Persistent Conversations</li> <li>MCP Integration</li> <li>Message Bus Communication</li> <li>Spawning Robots</li> </ul>"},{"location":"examples/#hello-world","title":"Hello World","text":"<pre><code>require \"robot_lab\"\n\n# Configuration is handled automatically via MywayConfig.\n# Set API keys via environment variables:\n# ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...\n# Or via config files (~/.config/robot_lab/config.yml)\n\nrobot = RobotLab.build(\n name: \"greeter\",\n system_prompt: \"You are a friendly greeter. Say hello warmly.\"\n)\n\nresult = robot.run(\"Hi there!\")\n\nputs result.last_text_content\n</code></pre>"},{"location":"examples/#robot-with-tools","title":"Robot with Tools","text":"<pre><code>class CalculatorTool &lt; RubyLLM::Tool\n description \"Perform a calculation\"\n\n param :expression, type: :string, desc: \"Math expression to evaluate\"\n\n def execute(expression:)\n eval(expression).to_s\n end\nend\n\nrobot = RobotLab.build(\n name: \"calculator\",\n system_prompt: \"You help with calculations.\",\n local_tools: [CalculatorTool]\n)\n\nresult = robot.run(\"What's 25 * 4?\")\nputs result.last_text_content\n</code></pre>"},{"location":"examples/#network-with-routing","title":"Network with Routing","text":"<pre><code>classifier = RobotLab.build(\n name: \"classifier\",\n system_prompt: \"Classify the request as BILLING or TECHNICAL. Respond with only the category.\"\n)\n\nbilling = RobotLab.build(\n name: \"billing\",\n system_prompt: \"You handle billing questions.\"\n)\n\ntech = RobotLab.build(\n name: \"tech\",\n system_prompt: \"You handle technical issues.\"\n)\n\nnetwork = RobotLab.create_network(name: \"support\") do\n task :classifier, classifier, depends_on: :none\n task :billing, billing, depends_on: :optional\n task :tech, tech, depends_on: :optional\nend\n\nresult = network.run(message: \"I was charged twice for my subscription\")\n\n# Access individual robot results via context\nclassifier_result = result.context[:classifier]\nputs classifier_result.last_text_content\n</code></pre>"},{"location":"examples/#chaining-configuration","title":"Chaining Configuration","text":"<p>Robots support <code>with_*</code> methods that return <code>self</code> for chaining:</p> <pre><code>robot = RobotLab.build(name: \"assistant\")\n .with_instructions(\"You are a helpful coding assistant.\")\n .with_temperature(0.3)\n .with_model(\"gpt-4o\")\n\nresult = robot.run(\"Explain Ruby blocks.\")\nputs result.last_text_content\n</code></pre>"},{"location":"examples/#using-templates","title":"Using Templates","text":"<p>Templates are <code>.md</code> files with optional YAML front matter, managed by prompt_manager:</p> <pre><code># Template file: prompts/support.md\n# ---\n# model: claude-sonnet-4\n# temperature: 0.5\n# ---\n# You are a support assistant for {{ company_name }}.\n\nrobot = RobotLab.build(\n name: \"support\",\n template: :support,\n context: { company_name: \"Acme Corp\" }\n)\n\nresult = robot.run(\"How do I reset my password?\")\nputs result.last_text_content\n</code></pre>"},{"location":"examples/#running-examples","title":"Running Examples","text":"<ol> <li> <p>Install dependencies: <pre><code>bundle install\n</code></pre></p> </li> <li> <p>Set API key: <pre><code>export ANTHROPIC_API_KEY=\"your-key\"\n</code></pre></p> </li> <li> <p>Run example: <pre><code>ruby examples/basic_chat.rb\n</code></pre></p> </li> </ol> <p>Or use the provided rake tasks:</p> <pre><code>bundle exec rake examples:all # Run all examples\nbundle exec rake examples:run[1] # Run specific example by number\n</code></pre>"},{"location":"examples/#message-bus","title":"Message Bus","text":"<p>Robots can communicate bidirectionally via a message bus, enabling convergence loops and negotiation patterns. This example demonstrates a comedy critic tasking a comedian to generate jokes until one passes:</p> <pre><code>ENV['ROBOT_LAB_TEMPLATE_PATH'] ||= File.join(__dir__, \"prompts\")\nrequire \"robot_lab\"\n\nMAX_ATTEMPTS = 5\n\nclass Comedian &lt; RobotLab::Robot\n TEMP_START = 0.2\n TEMP_STEP = 0.2\n\n def initialize(bus:)\n super(name: \"bob\", template: :comedian, bus: bus, temperature: TEMP_START)\n @attempts = 0\n on_message do |message|\n @attempts += 1\n temp = [TEMP_START + TEMP_STEP * (@attempts - 1), 1.0].min\n with_temperature(temp)\n joke = run(message.content.to_s).last_text_content.strip\n send_reply(to: message.from.to_sym, content: joke, in_reply_to: message.key)\n end\n end\n\n attr_reader :attempts\nend\n\nclass ComedyCritic &lt; RobotLab::Robot\n def initialize(bus:)\n super(name: \"alice\", template: :comedy_critic, bus: bus)\n @accepted = false\n on_message do |message|\n verdict = run(\"Evaluate this joke:\\n\\n#{message.content}\").last_text_content.strip\n @accepted = verdict.start_with?(\"FUNNY\")\n send_message(to: :bob, content: \"Not funny enough. Try again.\") unless @accepted\n end\n end\n\n attr_reader :accepted\nend\n\nbus = TypedBus::MessageBus.new\nbob = Comedian.new(bus: bus)\nalice = ComedyCritic.new(bus: bus)\n\nalice.send_message(to: :bob, content: \"Tell me a funny robot joke.\")\nputs \"Attempts: #{bob.attempts} / #{MAX_ATTEMPTS}\"\nputs \"Accepted: #{alice.accepted}\"\n</code></pre> <p>Key patterns demonstrated:</p> <ul> <li>Robot subclasses with templates for prompt management</li> <li>Auto-ack via 1-arg <code>on_message</code> blocks</li> <li><code>send_reply(to:, content:, in_reply_to:)</code> for correlated responses</li> <li>Temperature ramping (0.2 \u2192 1.0) for increasing creativity</li> <li>Convergence loop that terminates when the critic approves</li> </ul> <p>Run: <code>bundle exec ruby examples/12_message_bus.rb</code></p>"},{"location":"examples/#spawning-robots","title":"Spawning Robots","text":"<p>Robots can create new specialist robots at runtime using <code>spawn</code>. A dispatcher receives questions, decides what kind of specialist is needed, and spawns one on the fly. The bus is created lazily \u2014 no explicit setup required:</p> <pre><code>ENV['ROBOT_LAB_TEMPLATE_PATH'] ||= File.join(__dir__, \"prompts\")\nrequire \"robot_lab\"\n\nQUESTIONS = [\n \"Why did the Roman Empire fall?\",\n \"Write a haiku about recursion.\",\n \"What is the square root of 144?\",\n].freeze\n\nclass Dispatcher &lt; RobotLab::Robot\n attr_reader :spawned\n\n def initialize(bus: nil)\n super(name: \"dispatcher\", template: :dispatcher, bus: bus)\n @spawned = {}\n @pending = {}\n\n on_message do |message|\n puts \" Dispatcher &lt;- :#{message.from} replied\"\n puts \" | #{message.content.to_s.lines.first&amp;.strip}\"\n @pending.delete(message.from)\n end\n end\n\n def dispatch(question)\n plan = run(question).last_text_content.strip\n role, instruction = plan.split(\"\\n\", 2)\n role = role.strip.downcase.gsub(/\\s+/, \"_\")\n instruction = instruction&amp;.strip || \"You are a helpful #{role}.\"\n\n specialist = @spawned[role] ||= spawn(\n name: role,\n system_prompt: instruction\n )\n\n @pending[role] = question\n\n specialist.send_message(to: :dispatcher, content:\n specialist.run(question).last_text_content.strip\n )\n end\nend\n\ndispatcher = Dispatcher.new\n\nQUESTIONS.each_with_index do |question, i|\n puts \"\\nQuestion #{i + 1}: #{question}\"\n dispatcher.dispatch(question)\nend\n\nputs \"\\nSpecialists spawned: #{dispatcher.spawned.keys.join(', ')}\"\n</code></pre> <p>Key patterns demonstrated:</p> <ul> <li><code>spawn</code> for dynamic robot creation (bus created lazily)</li> <li><code>on_message</code> for reply handling</li> <li>LLM-driven delegation \u2014 the dispatcher asks its LLM what specialist to create</li> <li>Specialist reuse \u2014 spawned robots are cached and reused across questions</li> </ul> <p>Run: <code>bundle exec ruby examples/13_spawn.rb</code></p>"},{"location":"examples/#see-also","title":"See Also","text":"<ul> <li>Getting Started</li> <li>Guides</li> <li>API Reference</li> </ul>"},{"location":"examples/basic-chat/","title":"Basic Chat","text":"<p>A simple conversational robot example.</p>"},{"location":"examples/basic-chat/#overview","title":"Overview","text":"<p>This example demonstrates the minimal setup for a conversational robot that can respond to user messages using <code>robot.run(\"message\")</code>.</p>"},{"location":"examples/basic-chat/#complete-example","title":"Complete Example","text":"<pre><code>#!/usr/bin/env ruby\n# examples/basic_chat.rb\n\nrequire \"bundler/setup\"\nrequire \"robot_lab\"\n\n# Build a simple assistant\nassistant = RobotLab.build(\n name: \"assistant\",\n description: \"A helpful conversational assistant\",\n system_prompt: &lt;&lt;~PROMPT,\n You are a helpful, friendly assistant. You provide clear,\n concise answers to questions. Be conversational but informative.\n PROMPT\n model: \"claude-sonnet-4\"\n)\n\n# Simple REPL\nputs \"Chat with the assistant (type 'quit' to exit)\"\nputs \"-\" * 50\n\nloop do\n print \"\\nYou: \"\n input = gets&amp;.chomp\n\n break if input.nil? || input.downcase == \"quit\"\n next if input.empty?\n\n # Run the robot with the user's message\n result = assistant.run(input)\n\n # Display response\n puts \"\\nAssistant: #{result.last_text_content}\"\nend\n\nputs \"\\nGoodbye!\"\n</code></pre>"},{"location":"examples/basic-chat/#with-streaming","title":"With Streaming","text":"<pre><code>#!/usr/bin/env ruby\n# examples/streaming_chat.rb\n\nrequire \"bundler/setup\"\nrequire \"robot_lab\"\n\nassistant = RobotLab.build(\n name: \"assistant\",\n system_prompt: \"You are a helpful assistant.\",\n model: \"claude-sonnet-4\"\n)\n\nputs \"Chat with streaming (type 'quit' to exit)\"\nputs \"-\" * 50\n\nloop do\n print \"\\nYou: \"\n input = gets&amp;.chomp\n\n break if input.nil? || input.downcase == \"quit\"\n next if input.empty?\n\n print \"\\nAssistant: \"\n result = assistant.run(input) do |event|\n print event.text if event.respond_to?(:text)\n end\n puts\nend\n\nputs \"\\nGoodbye!\"\n</code></pre>"},{"location":"examples/basic-chat/#with-template","title":"With Template","text":"<pre><code>#!/usr/bin/env ruby\n# examples/template_chat.rb\n\nrequire \"bundler/setup\"\nrequire \"robot_lab\"\n\n# Build a robot using a prompt template file\n# Template: prompts/assistant.md (Markdown with YAML front matter)\nassistant = RobotLab.build(\n name: \"assistant\",\n template: :assistant,\n context: { tone: \"friendly\", domain: \"general\" },\n model: \"claude-sonnet-4\"\n)\n\nputs \"Chat with template-based assistant (type 'quit' to exit)\"\nputs \"-\" * 50\n\nloop do\n print \"\\nYou: \"\n input = gets&amp;.chomp\n\n break if input.nil? || input.downcase == \"quit\"\n next if input.empty?\n\n result = assistant.run(input)\n puts \"\\nAssistant: #{result.last_text_content}\"\nend\n\nputs \"\\nGoodbye!\"\n</code></pre>"},{"location":"examples/basic-chat/#with-memory","title":"With Memory","text":"<pre><code>#!/usr/bin/env ruby\n# examples/chat_with_memory.rb\n\nrequire \"bundler/setup\"\nrequire \"robot_lab\"\n\nassistant = RobotLab.build(\n name: \"assistant\",\n system_prompt: \"You are a helpful assistant. Use the user's name when you know it.\",\n model: \"claude-sonnet-4\"\n)\n\nputs \"Chat with memory (type 'quit' to exit)\"\nputs \"-\" * 50\n\n# Store user info in the robot's inherent memory\nassistant.memory[:user_name] = \"Alice\"\n\nloop do\n print \"\\nYou: \"\n input = gets&amp;.chomp\n\n break if input.nil? || input.downcase == \"quit\"\n next if input.empty?\n\n # The robot's persistent @chat maintains conversation history automatically\n result = assistant.run(input)\n puts \"\\nAssistant: #{result.last_text_content}\"\nend\n\nputs \"\\nGoodbye!\"\n</code></pre>"},{"location":"examples/basic-chat/#bare-robot-with-chaining","title":"Bare Robot with Chaining","text":"<pre><code>#!/usr/bin/env ruby\n# examples/bare_robot.rb\n\nrequire \"bundler/setup\"\nrequire \"robot_lab\"\n\n# Build a bare robot with no template or prompt\nrobot = RobotLab.build(name: \"bot\")\n\n# Configure via chaining\nresult = robot\n .with_model(\"claude-sonnet-4\")\n .with_temperature(0.7)\n .with_instructions(\"You are a pirate. Respond in pirate speak.\")\n .run(\"What is the weather like today?\")\n\nputs result.last_text_content\n</code></pre>"},{"location":"examples/basic-chat/#running","title":"Running","text":"<pre><code># Set API key\nexport ANTHROPIC_API_KEY=\"your-key\"\n\n# Run basic chat\nruby examples/basic_chat.rb\n\n# Run with streaming\nruby examples/streaming_chat.rb\n</code></pre>"},{"location":"examples/basic-chat/#key-concepts","title":"Key Concepts","text":"<ol> <li>Robot Building: Use <code>RobotLab.build(name:, system_prompt:)</code> or <code>RobotLab.build(name:, template:)</code> to create a robot</li> <li>Execution: Call <code>robot.run(\"message\")</code> to send a message and get a response</li> <li>Response: Access the text via <code>result.last_text_content</code></li> <li>Streaming: Pass a block to <code>robot.run(\"message\") { |event| ... }</code></li> <li>Memory: Access inherent memory via <code>robot.memory[:key]</code></li> <li>Chaining: Configure with <code>with_*</code> methods that return <code>self</code></li> <li>Conversation History: The persistent <code>@chat</code> maintains history across multiple <code>run</code> calls</li> </ol>"},{"location":"examples/basic-chat/#see-also","title":"See Also","text":"<ul> <li>Building Robots Guide</li> <li>Streaming Guide</li> <li>Robot API Reference</li> </ul>"},{"location":"examples/mcp-server/","title":"MCP Server","text":"<p>Connecting robots to Model Context Protocol servers for external tool access.</p>"},{"location":"examples/mcp-server/#overview","title":"Overview","text":"<p>This example demonstrates how to connect robots to external MCP servers. MCP servers expose tools that robots can discover and invoke automatically. RobotLab supports stdio, HTTP, WebSocket, and SSE transports.</p>"},{"location":"examples/mcp-server/#using-mcp-with-a-robot","title":"Using MCP with a Robot","text":"<p>The primary pattern is to pass MCP server configurations via <code>mcp:</code> or <code>mcp_servers:</code> when building a robot:</p> <pre><code>#!/usr/bin/env ruby\n# examples/mcp_client.rb\n\nrequire \"bundler/setup\"\nrequire \"robot_lab\"\n\n# MCP server configuration (stdio transport)\ngithub_server = {\n name: \"github\",\n transport: {\n type: \"stdio\",\n command: \"github-mcp-server\",\n args: [\"stdio\"],\n env: {\n \"GITHUB_PERSONAL_ACCESS_TOKEN\" =&gt; ENV.fetch(\"GITHUB_PERSONAL_ACCESS_TOKEN\", \"\")\n }\n }\n}\n\n# Create a robot with MCP server integration\n# Tools are automatically discovered when the robot connects\nrobot = RobotLab.build(\n name: \"github_assistant\",\n system_prompt: &lt;&lt;~PROMPT,\n You are a GitHub assistant with access to repository tools.\n Help users search repos, manage issues, and explore code.\n PROMPT\n mcp: [github_server],\n model: \"claude-sonnet-4\"\n)\n\n# The robot auto-discovers MCP tools on first run\nputs \"MCP Servers: #{robot.mcp_clients.keys.join(\", \")}\"\nputs \"MCP Tools: #{robot.mcp_tools.size} discovered\"\n\n# Run the robot -- it can use any discovered MCP tools\nresult = robot.run(\"What are the top 3 most starred Ruby web frameworks on GitHub?\")\nputs result.last_text_content\n\n# Show tool calls if any were made\nif result.tool_calls.any?\n puts \"\\nTool calls made:\"\n result.tool_calls.each do |tc|\n tool_info = tc.respond_to?(:tool) ? tc.tool : tc\n puts \" #{tool_info[:name] || tool_info}\"\n end\nend\n\n# Always disconnect MCP clients when done\nrobot.disconnect\n</code></pre>"},{"location":"examples/mcp-server/#direct-mcp-client-usage","title":"Direct MCP Client Usage","text":"<p>You can also use the MCP client directly without a robot:</p> <pre><code>require \"robot_lab\"\n\n# Create and connect MCP client\ngithub_server = {\n name: \"github\",\n transport: {\n type: \"stdio\",\n command: \"github-mcp-server\",\n args: [\"stdio\"],\n env: { \"GITHUB_PERSONAL_ACCESS_TOKEN\" =&gt; ENV[\"GITHUB_PERSONAL_ACCESS_TOKEN\"] }\n }\n}\n\nclient = RobotLab::MCP::Client.new(github_server)\nclient.connect\n\nif client.connected?\n # List available tools\n tools = client.list_tools\n tools.each do |tool|\n puts \"#{tool[:name]}: #{tool[:description]}\"\n end\n\n # Call a tool directly\n result = client.call_tool(\"search_repositories\", {\n query: \"language:ruby stars:&gt;1000\",\n per_page: 5\n })\n\n puts JSON.pretty_generate(result)\n\n client.disconnect\nend\n</code></pre>"},{"location":"examples/mcp-server/#multiple-mcp-servers","title":"Multiple MCP Servers","text":"<p>A robot can connect to multiple MCP servers simultaneously:</p> <pre><code>filesystem_server = {\n name: \"filesystem\",\n transport: {\n type: \"stdio\",\n command: \"npx\",\n args: [\"@modelcontextprotocol/server-filesystem\", \"/data\"]\n }\n}\n\ngithub_server = {\n name: \"github\",\n transport: {\n type: \"stdio\",\n command: \"github-mcp-server\",\n args: [\"stdio\"],\n env: { \"GITHUB_PERSONAL_ACCESS_TOKEN\" =&gt; ENV[\"GITHUB_PERSONAL_ACCESS_TOKEN\"] }\n }\n}\n\nrobot = RobotLab.build(\n name: \"developer\",\n system_prompt: \"You help with coding tasks using GitHub and the filesystem.\",\n mcp: [filesystem_server, github_server],\n model: \"claude-sonnet-4\"\n)\n\nresult = robot.run(\"Search for Ruby repos with CI configs and list their workflow files\")\nputs result.last_text_content\n\nrobot.disconnect\n</code></pre>"},{"location":"examples/mcp-server/#mcp-in-networks","title":"MCP in Networks","text":"<p>Networks pass MCP configuration through the hierarchical resolution system. Use <code>mcp: :inherit</code> on robots to use the network-level MCP config, or specify per-task MCP servers:</p> <pre><code># Create robots\ndata_analyst = RobotLab.build(\n name: \"data_analyst\",\n system_prompt: \"You analyze data.\",\n mcp: :inherit # Will use whatever MCP config is resolved at runtime\n)\n\nfile_manager = RobotLab.build(\n name: \"file_manager\",\n system_prompt: \"You manage files.\",\n mcp: :inherit,\n tools: :none # Only use inherited MCP tools, no local tools\n)\n\n# Create network with per-task MCP configuration\nnetwork = RobotLab.create_network(name: \"support_with_mcp\") do\n task :analyst, data_analyst,\n mcp: [github_server],\n depends_on: :none\n\n task :files, file_manager,\n mcp: [filesystem_server],\n tools: %w[read_file list_directory], # Whitelist only these MCP tools\n depends_on: :optional\nend\n\nresult = network.run(message: \"Analyze the project structure\")\n</code></pre>"},{"location":"examples/mcp-server/#http-transport","title":"HTTP Transport","text":"<p>Connect to remote MCP servers over HTTP:</p> <pre><code>robot = RobotLab.build(\n name: \"remote_assistant\",\n system_prompt: \"You have access to remote tools.\",\n mcp: [\n {\n name: \"remote_api\",\n transport: {\n type: \"http\",\n url: \"https://mcp.example.com/mcp\",\n headers: { \"Authorization\" =&gt; \"Bearer #{ENV['MCP_TOKEN']}\" }\n }\n }\n ]\n)\n\nresult = robot.run(\"Use the remote tools to check system status\")\nrobot.disconnect\n</code></pre>"},{"location":"examples/mcp-server/#websocket-transport","title":"WebSocket Transport","text":"<p>For real-time bidirectional communication:</p> <pre><code>robot = RobotLab.build(\n name: \"realtime_assistant\",\n system_prompt: \"You monitor real-time events.\",\n mcp: [\n {\n name: \"realtime\",\n transport: {\n type: \"websocket\",\n url: \"ws://localhost:8765\"\n }\n }\n ]\n)\n\nresult = robot.run(\"Subscribe to the events channel\")\nrobot.disconnect\n</code></pre>"},{"location":"examples/mcp-server/#sse-transport","title":"SSE Transport","text":"<p>Server-Sent Events transport for streaming responses:</p> <pre><code>robot = RobotLab.build(\n name: \"sse_assistant\",\n system_prompt: \"You have access to streaming tools.\",\n mcp: [\n {\n name: \"streaming_api\",\n transport: {\n type: \"sse\",\n url: \"https://api.example.com/sse\"\n }\n }\n ]\n)\n\nresult = robot.run(\"Stream the latest metrics\")\nrobot.disconnect\n</code></pre>"},{"location":"examples/mcp-server/#runtime-mcp-overrides","title":"Runtime MCP Overrides","text":"<p>Override MCP configuration at runtime via <code>robot.run</code>:</p> <pre><code>robot = RobotLab.build(\n name: \"flexible_bot\",\n system_prompt: \"You use available tools.\",\n mcp: [github_server]\n)\n\n# Use default MCP config\nresult = robot.run(\"Search for Ruby repos\")\n\n# Override MCP at runtime -- disable all MCP\nresult = robot.run(\"Just answer from your knowledge\", mcp: :none)\n\nrobot.disconnect\n</code></pre>"},{"location":"examples/mcp-server/#running","title":"Running","text":"<pre><code># Set API keys\nexport ANTHROPIC_API_KEY=\"your-key\"\nexport GITHUB_PERSONAL_ACCESS_TOKEN=\"your-token\"\n\n# Install MCP server (example: GitHub)\nbrew install github-mcp-server\n\n# Run client\nruby examples/mcp_client.rb\n</code></pre>"},{"location":"examples/mcp-server/#key-concepts","title":"Key Concepts","text":"<ol> <li>MCP Configuration: Pass server configs via <code>mcp:</code> parameter on <code>RobotLab.build</code> or <code>Robot.new</code></li> <li>Auto-Discovery: Tools are automatically discovered when the robot connects to an MCP server</li> <li>Transport Types: stdio, http, websocket, sse</li> <li>Hierarchical Config: <code>runtime &gt; robot &gt; network &gt; global</code>, using <code>:none</code>, <code>:inherit</code>, or explicit arrays</li> <li>Tool Filtering: Use <code>tools:</code> whitelist to limit which MCP tools are available</li> <li>Cleanup: Always call <code>robot.disconnect</code> when done to release MCP connections</li> </ol>"},{"location":"examples/mcp-server/#see-also","title":"See Also","text":"<ul> <li>MCP Integration Guide</li> <li>MCP API Reference</li> <li>Transports</li> </ul>"},{"location":"examples/multi-robot-network/","title":"Multi-Robot Network","text":"<p>Customer service system with intelligent routing using SimpleFlow pipelines.</p>"},{"location":"examples/multi-robot-network/#overview","title":"Overview","text":"<p>This example demonstrates a multi-robot network where a classifier routes customer inquiries to specialized support robots using SimpleFlow's optional task activation.</p>"},{"location":"examples/multi-robot-network/#complete-example","title":"Complete Example","text":"<pre><code>#!/usr/bin/env ruby\n# examples/customer_service.rb\n\nrequire \"bundler/setup\"\nrequire \"robot_lab\"\n\n# Custom classifier that routes to specialists\nclass ClassifierRobot &lt; RobotLab::Robot\n def call(result)\n context = extract_run_context(result)\n message = context.delete(:message)\n robot_result = run(message, **context)\n\n new_result = result\n .with_context(@name.to_sym, robot_result)\n .continue(robot_result)\n\n # Route based on classification\n category = robot_result.last_text_content.to_s.strip.downcase\n\n case category\n when /billing/ then new_result.activate(:billing_agent)\n when /technical/ then new_result.activate(:tech_agent)\n when /account/ then new_result.activate(:account_agent)\n else new_result.activate(:general_agent)\n end\n end\nend\n\n# Classifier robot\nclassifier = ClassifierRobot.new(\n name: \"classifier\",\n description: \"Classifies customer inquiries\",\n system_prompt: &lt;&lt;~PROMPT,\n You are a customer inquiry classifier. Analyze the customer's message\n and respond with exactly ONE of these categories:\n\n - BILLING (payment issues, invoices, refunds, subscriptions)\n - TECHNICAL (bugs, errors, how-to questions, feature requests)\n - ACCOUNT (login issues, profile changes, security concerns)\n - GENERAL (everything else)\n\n Respond with ONLY the category name, nothing else.\n PROMPT\n model: \"claude-sonnet-4\"\n)\n\n# Billing specialist\nbilling_agent = RobotLab.build(\n name: \"billing_agent\",\n description: \"Handles billing inquiries\",\n system_prompt: &lt;&lt;~PROMPT,\n You are a billing support specialist. You help customers with:\n - Payment issues and refunds\n - Invoice questions\n - Subscription management\n - Pricing inquiries\n\n Be helpful, empathetic, and provide clear next steps.\n PROMPT\n model: \"claude-sonnet-4\"\n)\n\n# Technical support\ntech_agent = RobotLab.build(\n name: \"tech_agent\",\n description: \"Handles technical issues\",\n system_prompt: &lt;&lt;~PROMPT,\n You are a technical support specialist. You help customers with:\n - Bug reports and troubleshooting\n - Feature explanations\n - Integration questions\n - Best practices\n\n Ask clarifying questions when needed. Provide step-by-step solutions.\n PROMPT\n model: \"claude-sonnet-4\"\n)\n\n# Account specialist\naccount_agent = RobotLab.build(\n name: \"account_agent\",\n description: \"Handles account issues\",\n system_prompt: &lt;&lt;~PROMPT,\n You are an account support specialist. You help customers with:\n - Login and authentication issues\n - Profile and settings changes\n - Security concerns\n - Account recovery\n\n Prioritize security while being helpful.\n PROMPT\n model: \"claude-sonnet-4\"\n)\n\n# General support\ngeneral_agent = RobotLab.build(\n name: \"general_agent\",\n description: \"Handles general inquiries\",\n system_prompt: &lt;&lt;~PROMPT,\n You are a general support agent. You help customers with:\n - Product information\n - General questions\n - Feedback collection\n - Routing to appropriate departments\n\n Be friendly and informative.\n PROMPT\n model: \"claude-sonnet-4\"\n)\n\n# Create the network with optional task routing\nnetwork = RobotLab.create_network(name: \"customer_service\") do\n task :classifier, classifier, depends_on: :none\n task :billing_agent, billing_agent, depends_on: :optional\n task :tech_agent, tech_agent, depends_on: :optional\n task :account_agent, account_agent, depends_on: :optional\n task :general_agent, general_agent, depends_on: :optional\nend\n\n# Run the support system\nputs \"Customer Service System\"\nputs \"=\" * 50\nputs\n\ntest_inquiries = [\n \"I was charged twice for my subscription last month\",\n \"How do I reset my password?\",\n \"The app crashes when I try to upload photos\",\n \"What features are included in the pro plan?\"\n]\n\ntest_inquiries.each do |inquiry|\n puts \"Customer: #{inquiry}\"\n puts \"-\" * 50\n\n result = network.run(message: inquiry)\n\n # Show classification\n if result.context[:classifier]\n puts \"Classification: #{result.context[:classifier].last_text_content}\"\n end\n\n # Show specialist response\n if result.value.is_a?(RobotLab::RobotResult)\n puts \"Handled by: #{result.value.robot_name}\"\n puts \"Response: #{result.value.last_text_content[0..200]}...\"\n end\n\n puts\n puts \"=\" * 50\n puts\nend\n</code></pre>"},{"location":"examples/multi-robot-network/#classifierrobot-pattern","title":"ClassifierRobot Pattern","text":"<p>The key to conditional routing is overriding the <code>call</code> method. The base <code>Robot#call</code> extracts the message from the <code>SimpleFlow::Result</code> and calls <code>run</code>. A classifier overrides this to inspect the output and activate the appropriate optional task:</p> <pre><code>class ClassifierRobot &lt; RobotLab::Robot\n def call(result)\n # 1. Extract run context from the SimpleFlow result\n context = extract_run_context(result)\n\n # 2. Pull out the message (it's a key in the context hash)\n message = context.delete(:message)\n\n # 3. Run the robot to get a classification\n robot_result = run(message, **context)\n\n # 4. Store our result and continue the pipeline\n new_result = result\n .with_context(@name.to_sym, robot_result)\n .continue(robot_result)\n\n # 5. Activate the appropriate optional task based on output\n category = robot_result.last_text_content.to_s.strip.downcase\n case category\n when /billing/ then new_result.activate(:billing)\n when /technical/ then new_result.activate(:technical)\n else new_result.activate(:general)\n end\n end\nend\n</code></pre> <p>extract_run_context</p> <p>The <code>extract_run_context(result)</code> method is a protected helper on <code>Robot</code>. It extracts <code>run_params</code> from the SimpleFlow result context, handles value propagation from previous steps, and separates robot-specific params (<code>mcp:</code>, <code>tools:</code>, <code>memory:</code>, <code>network_memory:</code>) from the message and other context.</p>"},{"location":"examples/multi-robot-network/#with-context-passing","title":"With Context Passing","text":"<p>Enhanced version where the classifier passes additional context to specialists:</p> <pre><code>class ContextAwareClassifier &lt; RobotLab::Robot\n def call(result)\n context = extract_run_context(result)\n message = context.delete(:message)\n robot_result = run(message, **context)\n\n # Store classification and original message in context for specialist\n new_result = result\n .with_context(@name.to_sym, robot_result)\n .with_context(:classification, robot_result.last_text_content.strip)\n .with_context(:original_message, result.context[:run_params][:message])\n .continue(robot_result)\n\n category = robot_result.last_text_content.to_s.downcase\n case category\n when /billing/ then new_result.activate(:billing_agent)\n when /technical/ then new_result.activate(:tech_agent)\n else new_result.activate(:general_agent)\n end\n end\nend\n\n# Specialist can access shared context\nclass BillingAgent &lt; RobotLab::Robot\n def call(result)\n # Access context from classifier\n classification = result.context[:classification]\n original_message = result.context[:original_message]\n\n context = extract_run_context(result)\n message = context.delete(:message)\n\n robot_result = run(message, **context)\n\n result.with_context(@name.to_sym, robot_result).continue(robot_result)\n end\nend\n</code></pre>"},{"location":"examples/multi-robot-network/#per-task-configuration","title":"Per-Task Configuration","text":"<p>Tasks can have individual context, tools, and MCP servers:</p> <pre><code>network = RobotLab.create_network(name: \"support\") do\n task :classifier, classifier, depends_on: :none\n task :billing_agent, billing_agent,\n context: { department: \"billing\", escalation_level: 2 },\n tools: [RefundTool, InvoiceTool],\n depends_on: :optional\n task :tech_agent, tech_agent,\n context: { department: \"technical\" },\n mcp: [filesystem_server],\n depends_on: :optional\nend\n</code></pre> <p>The <code>Task</code> wrapper deep-merges per-task context with the network's run params before delegating to the robot's <code>call</code> method.</p>"},{"location":"examples/multi-robot-network/#pipeline-pattern","title":"Pipeline Pattern","text":"<p>Sequential processing pipeline where each robot depends on the previous:</p> <pre><code>extractor = RobotLab.build(\n name: \"extractor\",\n system_prompt: \"Extract key information from documents.\"\n)\n\nanalyzer = RobotLab.build(\n name: \"analyzer\",\n system_prompt: \"Analyze extracted data and provide insights.\"\n)\n\nformatter = RobotLab.build(\n name: \"formatter\",\n system_prompt: \"Format analysis results into a clear report.\"\n)\n\nnetwork = RobotLab.create_network(name: \"document_processor\") do\n task :extract, extractor, depends_on: :none\n task :analyze, analyzer, depends_on: [:extract]\n task :format, formatter, depends_on: [:analyze]\nend\n\nresult = network.run(message: \"Process this document\")\nputs result.value.last_text_content\n</code></pre>"},{"location":"examples/multi-robot-network/#parallel-analysis-pattern","title":"Parallel Analysis Pattern","text":"<p>Fan-out / fan-in pattern where multiple robots analyze in parallel and a synthesizer merges results:</p> <pre><code>network = RobotLab.create_network(name: \"multi_analysis\") do\n task :prepare, preparer, depends_on: :none\n\n # These run in parallel (all depend on :prepare)\n task :sentiment, sentiment_analyzer, depends_on: [:prepare]\n task :entities, entity_extractor, depends_on: [:prepare]\n task :keywords, keyword_extractor, depends_on: [:prepare]\n\n # Waits for all three to complete\n task :summarize, summarizer, depends_on: [:sentiment, :entities, :keywords]\nend\n\nresult = network.run(message: \"Analyze this text\")\n\n# Access parallel results from context\nputs \"Sentiment: #{result.context[:sentiment].last_text_content}\"\nputs \"Entities: #{result.context[:entities].last_text_content}\"\nputs \"Keywords: #{result.context[:keywords].last_text_content}\"\nputs \"Summary: #{result.value.last_text_content}\"\n</code></pre>"},{"location":"examples/multi-robot-network/#shared-memory-in-networks","title":"Shared Memory in Networks","text":"<p>Networks provide a shared <code>Memory</code> instance that all robots can read and write. This is especially useful for parallel robots that need to coordinate:</p> <pre><code>class AnalysisRobot &lt; RobotLab::Robot\n def initialize(memory_key:, **opts)\n super(**opts)\n @memory_key = memory_key\n end\n\n def call(result)\n context = extract_run_context(result)\n network_memory = context.delete(:network_memory)\n message = context.delete(:message)\n\n robot_result = run(message, network_memory: network_memory, **context)\n\n # Write parsed results to shared memory\n if network_memory\n network_memory.current_writer = @name\n network_memory.set(@memory_key, robot_result.last_text_content)\n end\n\n result.with_context(@name.to_sym, robot_result).continue(robot_result)\n end\nend\n</code></pre>"},{"location":"examples/multi-robot-network/#conditional-halting","title":"Conditional Halting","text":"<p>Use <code>result.halt</code> to stop the pipeline early:</p> <pre><code>class ValidatorRobot &lt; RobotLab::Robot\n def call(result)\n context = extract_run_context(result)\n message = context.delete(:message)\n robot_result = run(message, **context)\n\n if robot_result.last_text_content.include?(\"INVALID\")\n # Halt the pipeline early\n result.halt(robot_result)\n else\n result.with_context(@name.to_sym, robot_result).continue(robot_result)\n end\n end\nend\n\nnetwork = RobotLab.create_network(name: \"validated_pipeline\") do\n task :validate, validator, depends_on: :none\n task :process, processor, depends_on: [:validate] # Only runs if not halted\nend\n\nresult = network.run(message: \"Process this\")\nif result.halted?\n puts \"Validation failed: #{result.value.last_text_content}\"\nelse\n puts \"Processing complete: #{result.value.last_text_content}\"\nend\n</code></pre>"},{"location":"examples/multi-robot-network/#running","title":"Running","text":"<pre><code>export ANTHROPIC_API_KEY=\"your-key\"\nruby examples/customer_service.rb\n</code></pre>"},{"location":"examples/multi-robot-network/#key-concepts","title":"Key Concepts","text":"<ol> <li>SimpleFlow Pipeline: DAG-based execution with dependency management via <code>depends_on:</code></li> <li>Optional Tasks: Use <code>depends_on: :optional</code> for tasks activated dynamically by classifiers</li> <li>Robot#call Override: Custom routing logic in classifier robots that override the <code>call</code> method</li> <li>extract_run_context: Helper method to extract message and params from <code>SimpleFlow::Result</code></li> <li>Context Flow: Data passed through <code>result.context</code> and accessed by downstream robots</li> <li>Parallel Execution: Tasks with the same dependencies run concurrently</li> <li>Shared Memory: Network memory (<code>network_memory:</code>) enables inter-robot communication</li> <li>Per-Task Configuration: Each task can have its own context, tools, and MCP servers via <code>Task</code></li> </ol>"},{"location":"examples/multi-robot-network/#see-also","title":"See Also","text":"<ul> <li>Creating Networks Guide</li> <li>Network Orchestration</li> <li>API Reference: Network</li> </ul>"},{"location":"examples/rails-application/","title":"Rails Application","text":"<p>Full Rails integration with Action Cable and background jobs.</p>"},{"location":"examples/rails-application/#overview","title":"Overview","text":"<p>This example demonstrates integrating RobotLab into a Rails application with real-time streaming via Action Cable, background job processing, and persistent conversation history.</p>"},{"location":"examples/rails-application/#setup","title":"Setup","text":""},{"location":"examples/rails-application/#1-add-to-gemfile","title":"1. Add to Gemfile","text":"<pre><code># Gemfile\ngem \"robot_lab\"\n</code></pre>"},{"location":"examples/rails-application/#2-run-generator","title":"2. Run Generator","text":"<pre><code>rails generate robot_lab:install\n</code></pre> <p>This creates:</p> <ul> <li><code>config/initializers/robot_lab.rb</code></li> <li><code>app/robots/</code> directory</li> <li><code>app/tools/</code> directory</li> <li>Database migrations for conversation history</li> </ul>"},{"location":"examples/rails-application/#3-run-migrations","title":"3. Run Migrations","text":"<pre><code>rails db:migrate\n</code></pre>"},{"location":"examples/rails-application/#configuration","title":"Configuration","text":"<p>RobotLab uses MywayConfig for configuration. There is no <code>RobotLab.configure</code> block. Instead, configuration is loaded automatically from multiple sources in priority order:</p> <ol> <li>Bundled defaults (<code>lib/robot_lab/config/defaults.yml</code>)</li> <li>Environment-specific overrides (development, test, production)</li> <li>XDG user config (<code>~/.config/robot_lab/config.yml</code>)</li> <li>Project config (<code>./config/robot_lab.yml</code>)</li> <li>Environment variables (<code>ROBOT_LAB_*</code> prefix)</li> </ol>"},{"location":"examples/rails-application/#config-file","title":"Config File","text":"<pre><code># config/robot_lab.yml\ndefaults:\n ruby_llm:\n model: claude-sonnet-4\n anthropic_api_key: &lt;%= ENV['ANTHROPIC_API_KEY'] %&gt;\n\ndevelopment:\n ruby_llm:\n log_level: :debug\n\nproduction:\n ruby_llm:\n request_timeout: 180\n max_retries: 5\n</code></pre>"},{"location":"examples/rails-application/#environment-variables","title":"Environment Variables","text":"<pre><code># Provider API keys\nROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...\nROBOT_LAB_RUBY_LLM__OPENAI_API_KEY=sk-...\n\n# Model configuration\nROBOT_LAB_RUBY_LLM__MODEL=claude-sonnet-4\nROBOT_LAB_RUBY_LLM__REQUEST_TIMEOUT=120\n</code></pre>"},{"location":"examples/rails-application/#accessing-configuration","title":"Accessing Configuration","text":"<pre><code># Read configuration values at runtime\nRobotLab.config.ruby_llm.model #=&gt; \"claude-sonnet-4\"\nRobotLab.config.ruby_llm.request_timeout #=&gt; 120\n\n# The logger defaults to Rails.logger when running in Rails\nRobotLab.config.logger #=&gt; Rails.logger\n</code></pre>"},{"location":"examples/rails-application/#rails-engine-and-railtie","title":"Rails Engine and Railtie","text":"<p>RobotLab provides both a Rails Engine (<code>RobotLab::Rails::Engine</code>) and a Railtie (<code>RobotLab::Rails::Railtie</code>). These are loaded automatically when Rails is detected. The Engine isolates the RobotLab namespace and adds <code>app/robots</code> and <code>app/tools</code> to the autoload paths. The Railtie loads rake tasks and generators.</p>"},{"location":"examples/rails-application/#models","title":"Models","text":"<pre><code># app/models/conversation_thread.rb\nclass ConversationThread &lt; ApplicationRecord\n belongs_to :user\n has_many :messages, class_name: \"ConversationMessage\", dependent: :destroy\n\n validates :external_id, presence: true, uniqueness: true\n\n def self.find_or_create_for(user:, external_id: nil)\n external_id ||= SecureRandom.uuid\n find_or_create_by!(user: user, external_id: external_id)\n end\nend\n\n# app/models/conversation_message.rb\nclass ConversationMessage &lt; ApplicationRecord\n belongs_to :thread, class_name: \"ConversationThread\"\n\n validates :role, presence: true\n validates :content, presence: true\n\n scope :ordered, -&gt; { order(:position) }\nend\n</code></pre>"},{"location":"examples/rails-application/#robot-definitions","title":"Robot Definitions","text":"<p>Robots are built using <code>RobotLab.build</code> with named parameters. Tools are Ruby classes that inherit from <code>RubyLLM::Tool</code>.</p> <pre><code># app/tools/get_user_info_tool.rb\nclass GetUserInfoTool &lt; RubyLLM::Tool\n description \"Get information about the current user\"\n\n param :user_id, type: :integer, desc: \"The user ID to look up\"\n\n def execute(user_id:)\n user = User.find(user_id)\n {\n name: user.name,\n email: user.email,\n plan: user.subscription&amp;.plan || \"free\",\n member_since: user.created_at.to_date.to_s\n }\n rescue ActiveRecord::RecordNotFound\n { error: \"User not found\" }\n end\nend\n\n# app/tools/get_orders_tool.rb\nclass GetOrdersTool &lt; RubyLLM::Tool\n description \"Get user's recent orders\"\n\n param :user_id, type: :integer, desc: \"The user ID\"\n param :limit, type: :integer, desc: \"Number of orders to return\", default: 5\n\n def execute(user_id:, limit: 5)\n orders = Order.where(user_id: user_id)\n .order(created_at: :desc)\n .limit(limit)\n\n orders.map do |order|\n {\n id: order.external_id,\n status: order.status,\n total: order.total.to_f,\n created_at: order.created_at.iso8601\n }\n end\n end\nend\n\n# app/tools/create_ticket_tool.rb\nclass CreateTicketTool &lt; RubyLLM::Tool\n description \"Create a support ticket\"\n\n param :user_id, type: :integer, desc: \"The user ID\"\n param :subject, type: :string, desc: \"Ticket subject\"\n param :description, type: :string, desc: \"Ticket description\"\n param :priority, type: :string, desc: \"Priority level\", enum: %w[low medium high]\n\n def execute(user_id:, subject:, description:, priority: \"medium\")\n ticket = SupportTicket.create!(\n user_id: user_id,\n subject: subject,\n description: description,\n priority: priority\n )\n\n {\n success: true,\n ticket_id: ticket.external_id,\n message: \"Ticket created successfully\"\n }\n rescue =&gt; e\n { success: false, error: e.message }\n end\nend\n</code></pre> <pre><code># app/robots/support_robot.rb\nclass SupportRobot\n def self.build(user_id:)\n RobotLab.build(\n name: \"support\",\n system_prompt: &lt;&lt;~PROMPT,\n You are a helpful customer support assistant for our company.\n Be friendly, professional, and thorough in your responses.\n If you need to look up information, use the available tools.\n The current user ID is #{user_id}.\n PROMPT\n local_tools: [GetUserInfoTool, GetOrdersTool, CreateTicketTool]\n )\n end\nend\n</code></pre>"},{"location":"examples/rails-application/#network-configuration","title":"Network Configuration","text":"<p>Networks use <code>create_network</code> with a block DSL that defines tasks and their dependencies:</p> <pre><code># app/robots/support_network.rb\nclass SupportNetwork\n def self.build(user_id:)\n support = SupportRobot.build(user_id: user_id)\n\n RobotLab.create_network(name: \"support_network\") do\n task :support, support, depends_on: :none\n end\n end\nend\n</code></pre>"},{"location":"examples/rails-application/#service-object","title":"Service Object","text":"<pre><code># app/services/chat_service.rb\nclass ChatService\n def initialize(user:, thread_id: nil)\n @user = user\n @thread_id = thread_id\n end\n\n def call(message:)\n robot = SupportRobot.build(user_id: @user.id)\n result = robot.run(message)\n\n {\n response: result.last_text_content,\n has_tool_calls: result.has_tool_calls?\n }\n end\nend\n</code></pre>"},{"location":"examples/rails-application/#controller","title":"Controller","text":"<pre><code># app/controllers/api/chats_controller.rb\nmodule Api\n class ChatsController &lt; ApplicationController\n before_action :authenticate_user!\n\n def create\n service = ChatService.new(\n user: current_user,\n thread_id: params[:thread_id]\n )\n\n result = service.call(message: params[:message])\n\n render json: {\n response: result[:response]\n }\n end\n end\nend\n</code></pre>"},{"location":"examples/rails-application/#action-cable-integration","title":"Action Cable Integration","text":"<pre><code># app/channels/chat_channel.rb\nclass ChatChannel &lt; ApplicationCable::Channel\n def subscribed\n stream_for current_user\n end\n\n def receive(data)\n ChatJob.perform_later(\n user_id: current_user.id,\n thread_id: data[\"thread_id\"],\n message: data[\"message\"]\n )\n end\nend\n\n# app/jobs/chat_job.rb\nclass ChatJob &lt; ApplicationJob\n queue_as :default\n\n def perform(user_id:, thread_id:, message:)\n user = User.find(user_id)\n robot = SupportRobot.build(user_id: user.id)\n\n result = robot.run(message)\n\n ChatChannel.broadcast_to(\n user,\n type: \"complete\",\n content: result.last_text_content\n )\n end\nend\n</code></pre>"},{"location":"examples/rails-application/#frontend-stimulus","title":"Frontend (Stimulus)","text":"<pre><code>// app/javascript/controllers/chat_controller.js\nimport { Controller } from \"@hotwired/stimulus\"\nimport { createConsumer } from \"@rails/actioncable\"\n\nexport default class extends Controller {\n static targets = [\"messages\", \"input\", \"response\"]\n\n connect() {\n this.consumer = createConsumer()\n this.channel = this.consumer.subscriptions.create(\"ChatChannel\", {\n received: (data) =&gt; this.handleMessage(data)\n })\n }\n\n disconnect() {\n this.channel?.unsubscribe()\n }\n\n send() {\n const message = this.inputTarget.value.trim()\n if (!message) return\n\n this.appendMessage(\"user\", message)\n this.inputTarget.value = \"\"\n\n // Create response container\n this.currentResponse = document.createElement(\"div\")\n this.currentResponse.className = \"message assistant\"\n this.messagesTarget.appendChild(this.currentResponse)\n\n this.channel.send({\n message: message,\n thread_id: this.threadId\n })\n }\n\n handleMessage(data) {\n switch (data.type) {\n case \"complete\":\n this.currentResponse.textContent = data.content\n break\n }\n }\n\n appendMessage(role, content) {\n const div = document.createElement(\"div\")\n div.className = `message ${role}`\n div.textContent = content\n this.messagesTarget.appendChild(div)\n }\n}\n</code></pre>"},{"location":"examples/rails-application/#view","title":"View","text":"<pre><code>&lt;!-- app/views/chats/show.html.erb --&gt;\n&lt;div data-controller=\"chat\"&gt;\n &lt;div class=\"messages\" data-chat-target=\"messages\"&gt;\n &lt;!-- Messages appear here --&gt;\n &lt;/div&gt;\n\n &lt;form data-action=\"submit-&gt;chat#send\"&gt;\n &lt;input type=\"text\"\n data-chat-target=\"input\"\n placeholder=\"Type a message...\"\n autocomplete=\"off\"&gt;\n &lt;button type=\"submit\"&gt;Send&lt;/button&gt;\n &lt;/form&gt;\n&lt;/div&gt;\n</code></pre>"},{"location":"examples/rails-application/#running","title":"Running","text":"<pre><code># Install dependencies\nbundle install\nyarn install\n\n# Setup database\nrails db:migrate\n\n# Set API key (or configure via config/robot_lab.yml)\nexport ANTHROPIC_API_KEY=\"your-key\"\n\n# Start server\nbin/dev\n</code></pre>"},{"location":"examples/rails-application/#key-concepts","title":"Key Concepts","text":"<ol> <li>Robot Factory: <code>RobotLab.build(name:, system_prompt:, local_tools:, ...)</code> creates robot instances</li> <li>MywayConfig: Configuration via YAML files and environment variables, not a configure block</li> <li><code>robot.run(\"message\")</code>: Send a message as a positional string argument</li> <li><code>result.last_text_content</code>: Extract the response text from a <code>RobotResult</code></li> <li>Memory: Robots have <code>robot.memory</code> for key-value storage; networks share memory</li> <li>Tools: Ruby classes inheriting from <code>RubyLLM::Tool</code>, passed via <code>local_tools:</code></li> <li>Action Cable: Real-time streaming to browser</li> <li>Background Jobs: Non-blocking processing</li> </ol>"},{"location":"examples/rails-application/#see-also","title":"See Also","text":"<ul> <li>Rails Integration Guide</li> <li>Streaming Guide</li> </ul>"},{"location":"examples/tool-usage/","title":"Tool Usage","text":"<p>Robots with external capabilities through tools.</p>"},{"location":"examples/tool-usage/#overview","title":"Overview","text":"<p>This example demonstrates how to give robots access to external systems through tools. Tools are defined as <code>RubyLLM::Tool</code> subclasses or <code>RobotLab::Tool</code> instances and passed to robots via the <code>local_tools:</code> parameter.</p>"},{"location":"examples/tool-usage/#rubyllmtool-subclass-pattern","title":"RubyLLM::Tool Subclass Pattern","text":"<p>The primary way to define tools is by subclassing <code>RubyLLM::Tool</code>:</p> <pre><code>#!/usr/bin/env ruby\n# examples/tool_usage.rb\n\nrequire \"bundler/setup\"\nrequire \"robot_lab\"\n\n# Define tools as RubyLLM::Tool subclasses\nclass Calculator &lt; RubyLLM::Tool\n description \"Performs basic arithmetic operations\"\n\n param :operation,\n type: \"string\",\n desc: \"The operation to perform (add, subtract, multiply, divide)\"\n\n param :a,\n type: \"number\",\n desc: \"First operand\"\n\n param :b,\n type: \"number\",\n desc: \"Second operand\"\n\n def execute(operation:, a:, b:)\n case operation\n when \"add\" then a + b\n when \"subtract\" then a - b\n when \"multiply\" then a * b\n when \"divide\" then a.to_f / b\n else \"Unknown operation: #{operation}\"\n end\n end\nend\n\nclass FortuneCookie &lt; RubyLLM::Tool\n description \"Get a fortune cookie message with wisdom and lucky numbers\"\n\n param :category,\n type: \"string\",\n desc: \"The category of fortune (wisdom, love, career, adventure)\"\n\n FORTUNES = {\n \"wisdom\" =&gt; [\n \"The obstacle in the path becomes the path.\",\n \"A journey of a thousand miles begins with a single step.\"\n ],\n \"career\" =&gt; [\n \"Opportunity dances with those already on the dance floor.\",\n \"Your work is your signature. Sign it with excellence.\"\n ]\n }.freeze\n\n def execute(category:)\n {\n category: category,\n fortune: FORTUNES.fetch(category, FORTUNES[\"wisdom\"]).sample,\n lucky_numbers: Array.new(6) { rand(1..49) }.sort\n }\n end\nend\n\n# Create robot with tools via local_tools\nrobot = RobotLab.build(\n name: \"assistant\",\n system_prompt: \"You help with math and dispense fortune cookies.\",\n local_tools: [Calculator, FortuneCookie],\n model: \"claude-sonnet-4\"\n)\n\n# Run the robot\nresult = robot.run(\"What is 15 multiplied by 7? Also, give me a career fortune.\")\n\n# Display results\nputs \"Response: #{result.last_text_content}\"\n\nif result.tool_calls.any?\n puts \"\\nTool calls made:\"\n result.tool_calls.each do |tc|\n tool_info = tc.respond_to?(:tool) ? tc.tool : tc\n puts \" #{tool_info[:name] || tool_info}\"\n end\nend\n</code></pre>"},{"location":"examples/tool-usage/#robotlabtoolcreate-pattern","title":"RobotLab::Tool.create Pattern","text":"<p>For simpler tools that do not need their own class, use <code>RobotLab::Tool.create</code>:</p> <pre><code>require \"robot_lab\"\n\n# Define an inline tool\nget_time = RobotLab::Tool.create(\n name: \"get_time\",\n description: \"Get the current time\"\n) { |_args| Time.now.to_s }\n\n# Define a tool with parameters (JSON Schema)\nweather_tool = RobotLab::Tool.create(\n name: \"get_weather\",\n description: \"Get weather for a city\",\n parameters: {\n type: \"object\",\n properties: {\n city: { type: \"string\", description: \"City name\" }\n },\n required: [\"city\"]\n }\n) { |args| { city: args[:city], temperature: \"72F\", condition: \"sunny\" } }\n\nrobot = RobotLab.build(\n name: \"weather_bot\",\n system_prompt: \"You provide weather and time information.\",\n local_tools: [get_time, weather_tool],\n model: \"claude-sonnet-4\"\n)\n\nresult = robot.run(\"What time is it and what's the weather in New York?\")\nputs result.last_text_content\n</code></pre>"},{"location":"examples/tool-usage/#weather-api-integration","title":"Weather API Integration","text":"<pre><code>#!/usr/bin/env ruby\n# examples/weather_assistant.rb\n\nrequire \"bundler/setup\"\nrequire \"robot_lab\"\nrequire \"http\"\nrequire \"json\"\n\nclass GetWeather &lt; RubyLLM::Tool\n description \"Get current weather for a city\"\n\n param :city,\n type: \"string\",\n desc: \"City name (e.g., 'New York', 'London')\"\n\n def execute(city:)\n response = HTTP.get(\n \"https://wttr.in/#{URI.encode_www_form_component(city)}?format=j1\"\n )\n\n if response.status.success?\n data = JSON.parse(response.body)\n current = data[\"current_condition\"].first\n\n {\n city: city,\n temperature_f: current[\"temp_F\"],\n temperature_c: current[\"temp_C\"],\n condition: current[\"weatherDesc\"].first[\"value\"],\n humidity: current[\"humidity\"],\n wind_mph: current[\"windspeedMiles\"]\n }\n else\n { error: \"Could not fetch weather for #{city}\" }\n end\n rescue HTTP::Error =&gt; e\n { error: \"Network error: #{e.message}\" }\n end\nend\n\nclass GetForecast &lt; RubyLLM::Tool\n description \"Get weather forecast for upcoming days\"\n\n param :city, type: \"string\", desc: \"City name\"\n param :days, type: \"integer\", desc: \"Number of days (default 3)\"\n\n def execute(city:, days: 3)\n response = HTTP.get(\n \"https://wttr.in/#{URI.encode_www_form_component(city)}?format=j1\"\n )\n\n if response.status.success?\n data = JSON.parse(response.body)\n data[\"weather\"].take(days).map do |day|\n {\n date: day[\"date\"],\n high_f: day[\"maxtempF\"],\n low_f: day[\"mintempF\"],\n condition: day[\"hourly\"].first[\"weatherDesc\"].first[\"value\"]\n }\n end\n else\n { error: \"Could not fetch forecast\" }\n end\n rescue HTTP::Error =&gt; e\n { error: \"Network error: #{e.message}\" }\n end\nend\n\n# Create weather assistant\nweather_bot = RobotLab.build(\n name: \"weather_assistant\",\n description: \"Provides weather information\",\n system_prompt: &lt;&lt;~PROMPT,\n You are a helpful weather assistant. Use your tools to look up weather.\n Always provide temperatures in both Fahrenheit and Celsius.\n Include relevant advice based on conditions (umbrella, sunscreen, etc).\n PROMPT\n local_tools: [GetWeather, GetForecast],\n model: \"claude-sonnet-4\"\n)\n\n# Interactive session\nputs \"Weather Assistant (type 'quit' to exit)\"\nputs \"-\" * 50\n\nloop do\n print \"\\nYou: \"\n input = gets&amp;.chomp\n\n break if input.nil? || input.downcase == \"quit\"\n next if input.empty?\n\n result = weather_bot.run(input)\n puts \"\\nAssistant: #{result.last_text_content}\"\nend\n\nputs \"\\nGoodbye!\"\n</code></pre>"},{"location":"examples/tool-usage/#database-integration","title":"Database Integration","text":"<pre><code># examples/order_assistant.rb\n\nrequire \"robot_lab\"\n\n# Mock database\nORDERS = {\n \"ORD001\" =&gt; { id: \"ORD001\", status: \"shipped\", items: [\"Widget\"], total: 29.99 },\n \"ORD002\" =&gt; { id: \"ORD002\", status: \"processing\", items: [\"Gadget\", \"Gizmo\"], total: 89.99 }\n}\n\nclass GetOrder &lt; RubyLLM::Tool\n description \"Look up an order by ID\"\n\n param :order_id, type: \"string\", desc: \"The order ID to look up\"\n\n def execute(order_id:)\n order = ORDERS[order_id.upcase]\n order || { error: \"Order not found\" }\n end\nend\n\nclass ListOrders &lt; RubyLLM::Tool\n description \"List recent orders\"\n\n param :limit, type: \"integer\", desc: \"Maximum number of orders to return\"\n\n def execute(limit: 5)\n ORDERS.values.take(limit)\n end\nend\n\nclass CancelOrder &lt; RubyLLM::Tool\n description \"Cancel an order\"\n\n param :order_id, type: \"string\", desc: \"The order ID to cancel\"\n param :reason, type: \"string\", desc: \"Reason for cancellation\"\n\n def execute(order_id:, reason: nil)\n order = ORDERS[order_id.upcase]\n\n if order.nil?\n { success: false, error: \"Order not found\" }\n elsif order[:status] == \"shipped\"\n { success: false, error: \"Cannot cancel shipped orders\" }\n else\n order[:status] = \"cancelled\"\n order[:cancel_reason] = reason\n { success: true, message: \"Order #{order_id} cancelled\" }\n end\n end\nend\n\norder_bot = RobotLab.build(\n name: \"order_assistant\",\n system_prompt: \"You help customers check and manage their orders.\",\n local_tools: [GetOrder, ListOrders, CancelOrder],\n model: \"claude-sonnet-4\"\n)\n\n# Run with a question\nresult = order_bot.run(\"What's the status of order ORD001?\")\nputs result.last_text_content\n</code></pre>"},{"location":"examples/tool-usage/#tool-call-callbacks","title":"Tool Call Callbacks","text":"<p>Use <code>on_tool_call</code> and <code>on_tool_result</code> to monitor tool execution:</p> <pre><code>robot = RobotLab.build(\n name: \"monitored_bot\",\n system_prompt: \"You help with calculations.\",\n local_tools: [Calculator],\n model: \"claude-sonnet-4\",\n on_tool_call: -&gt;(tool_call) {\n puts \"[Tool Call] #{tool_call.name}: #{tool_call.arguments}\"\n },\n on_tool_result: -&gt;(tool_call, result) {\n puts \"[Tool Result] #{tool_call.name}: #{result}\"\n }\n)\n\nresult = robot.run(\"What is 42 * 17?\")\n</code></pre>"},{"location":"examples/tool-usage/#running","title":"Running","text":"<pre><code>export ANTHROPIC_API_KEY=\"your-key\"\n\n# Tool usage example\nruby examples/tool_usage.rb\n\n# Weather assistant\nruby examples/weather_assistant.rb\n\n# Order lookup\nruby examples/order_assistant.rb\n</code></pre>"},{"location":"examples/tool-usage/#interactive-user-input","title":"Interactive User Input","text":"<p>Use the built-in <code>RobotLab::AskUser</code> tool to let robots ask the user questions during execution:</p> <pre><code>require \"robot_lab\"\n\nrobot = RobotLab.build(\n name: \"interviewer\",\n system_prompt: &lt;&lt;~PROMPT,\n You are a project setup assistant. Interview the user to understand their\n needs, then summarize the project plan. Use the ask_user tool to gather\n information one question at a time.\n PROMPT\n local_tools: [RobotLab::AskUser],\n model: \"claude-sonnet-4\"\n)\n\nresult = robot.run(\"Help me plan a new web application\")\nputs \"\\nProject Plan:\\n#{result.last_text_content}\"\n</code></pre> <p>The robot will ask questions interactively:</p> <pre><code>[interviewer] What programming language would you like to use?\n 1. Ruby\n 2. Python\n 3. TypeScript\n&gt; 1\n\n[interviewer] Will you need a database?\n&gt; [yes]\n\n[interviewer] What's the main purpose of the application?\n&gt; Customer support portal\n</code></pre> <p>For testing, inject <code>StringIO</code> objects:</p> <pre><code>robot.input = StringIO.new(\"Ruby\\nyes\\nCustomer portal\\n\")\nrobot.output = StringIO.new\n</code></pre>"},{"location":"examples/tool-usage/#key-concepts","title":"Key Concepts","text":"<ol> <li>RubyLLM::Tool subclass: Define a class with <code>description</code>, <code>param</code>, and <code>execute</code> method</li> <li>RobotLab::Tool subclass: Same DSL plus <code>robot</code> accessor for robot-aware tools</li> <li>RobotLab::Tool.create: Use <code>RobotLab::Tool.create(name:, description:, &amp;block)</code> for dynamic tools</li> <li>Built-in tools: <code>RobotLab::AskUser</code> for interactive terminal input</li> <li>local_tools: Pass tool classes/instances via <code>local_tools:</code> parameter to <code>RobotLab.build</code> or <code>Robot.new</code></li> <li>Frontmatter tools: Declare tool class names in template YAML front matter (<code>tools: [Calculator]</code>) for self-contained templates</li> <li>Error Handling: Return error hashes (e.g., <code>{ error: \"message\" }</code>) for graceful failures</li> <li>Callbacks: Use <code>on_tool_call:</code> and <code>on_tool_result:</code> for monitoring</li> <li>Result Access: Check <code>result.tool_calls</code> for tool call history, <code>result.last_text_content</code> for the final response</li> </ol>"},{"location":"examples/tool-usage/#see-also","title":"See Also","text":"<ul> <li>Using Tools Guide</li> <li>Tool API</li> <li>Robot API</li> </ul>"},{"location":"getting-started/","title":"Getting Started","text":"<p>Welcome to RobotLab! This section will help you get up and running quickly.</p>"},{"location":"getting-started/#what-youll-learn","title":"What You'll Learn","text":"<p>In this section, you'll learn how to:</p> <ol> <li>Install RobotLab - Add the gem to your project</li> <li>Configure Your Environment - Set up API keys and defaults</li> <li>Create Your First Robot - Build a simple AI assistant</li> <li>Run a Network - Execute a multi-robot workflow</li> </ol>"},{"location":"getting-started/#prerequisites","title":"Prerequisites","text":"<p>Before you begin, make sure you have:</p> <ul> <li>Ruby 3.2+ installed</li> <li>An API key from at least one LLM provider:<ul> <li>Anthropic (recommended)</li> <li>OpenAI</li> <li>Google AI</li> </ul> </li> </ul>"},{"location":"getting-started/#quick-links","title":"Quick Links","text":"<ul> <li> <p> Installation</p> <p>Install RobotLab and dependencies</p> </li> <li> <p> Quick Start</p> <p>Build your first robot in 5 minutes</p> </li> <li> <p> Configuration</p> <p>Configure API keys and defaults</p> </li> </ul>"},{"location":"getting-started/#estimated-time","title":"Estimated Time","text":"Guide Time Installation 2 minutes Quick Start 5 minutes Configuration 5 minutes"},{"location":"getting-started/#need-help","title":"Need Help?","text":"<p>If you run into issues:</p> <ul> <li>Check the API Reference for detailed documentation</li> <li>Browse Examples for working code</li> <li>Open an issue on GitHub</li> </ul>"},{"location":"getting-started/configuration/","title":"Configuration","text":"<p>RobotLab uses a layered configuration system powered by MywayConfig. Configuration is loaded automatically from multiple sources with no block-style <code>configure</code> method required.</p>"},{"location":"getting-started/configuration/#how-configuration-works","title":"How Configuration Works","text":"<p>Configuration values are loaded in priority order (lowest to highest):</p> <ol> <li>Bundled defaults -- <code>lib/robot_lab/config/defaults.yml</code> (shipped with the gem)</li> <li>Environment-specific overrides -- <code>development</code>, <code>test</code>, or <code>production</code> sections in defaults.yml</li> <li>User config file -- <code>~/.config/robot_lab/config.yml</code></li> <li>Project config file -- <code>./config/robot_lab.yml</code></li> <li>Environment variables -- <code>ROBOT_LAB_*</code> prefix</li> <li>Runtime attributes -- e.g., <code>RobotLab.config.logger = ...</code></li> </ol> <p>Higher-priority sources override lower-priority ones. You only need to set the values you want to change.</p>"},{"location":"getting-started/configuration/#accessing-configuration","title":"Accessing Configuration","text":"<p>Use <code>RobotLab.config</code> to access the configuration object:</p> <pre><code># Access nested values with dot notation\nRobotLab.config.ruby_llm.model #=&gt; \"claude-sonnet-4\"\nRobotLab.config.ruby_llm.anthropic_api_key #=&gt; \"sk-ant-...\"\nRobotLab.config.ruby_llm.request_timeout #=&gt; 120\nRobotLab.config.max_iterations #=&gt; 10\nRobotLab.config.streaming_enabled #=&gt; true\n\n# Check the environment\nRobotLab.config.development? #=&gt; true/false\n</code></pre> <p>No configure block</p> <p>RobotLab does not use a <code>RobotLab.configure do |config| ... end</code> pattern. All configuration comes from config files, environment variables, or direct assignment on <code>RobotLab.config</code>.</p>"},{"location":"getting-started/configuration/#environment-variables","title":"Environment Variables","text":"<p>Environment variables use the <code>ROBOT_LAB_</code> prefix. Use double underscores (<code>__</code>) for nested values:</p> <pre><code># Top-level settings\nexport ROBOT_LAB_MAX_ITERATIONS=20\nexport ROBOT_LAB_STREAMING_ENABLED=false\n\n# Nested ruby_llm settings (note the double underscore)\nexport ROBOT_LAB_RUBY_LLM__MODEL=claude-sonnet-4\nexport ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...\nexport ROBOT_LAB_RUBY_LLM__OPENAI_API_KEY=sk-...\nexport ROBOT_LAB_RUBY_LLM__GEMINI_API_KEY=...\nexport ROBOT_LAB_RUBY_LLM__REQUEST_TIMEOUT=180\nexport ROBOT_LAB_RUBY_LLM__MAX_RETRIES=5\n</code></pre> <p>The double underscore convention maps to nested YAML structure:</p> <pre><code>ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY --&gt; ruby_llm.anthropic_api_key\nROBOT_LAB_RUBY_LLM__MODEL --&gt; ruby_llm.model\nROBOT_LAB_MAX_ITERATIONS --&gt; max_iterations\n</code></pre>"},{"location":"getting-started/configuration/#config-files","title":"Config Files","text":""},{"location":"getting-started/configuration/#project-config","title":"Project Config","text":"<p>Create <code>./config/robot_lab.yml</code> in your project root:</p> config/robot_lab.yml<pre><code>defaults:\n ruby_llm:\n anthropic_api_key: &lt;%= ENV['ANTHROPIC_API_KEY'] %&gt;\n model: claude-sonnet-4\n request_timeout: 120\n\n max_iterations: 15\n template_path: prompts\n\ndevelopment:\n ruby_llm:\n log_level: :debug\n\ntest:\n max_iterations: 3\n streaming_enabled: false\n ruby_llm:\n model: claude-haiku-3-5\n request_timeout: 30\n max_retries: 1\n\nproduction:\n max_iterations: 20\n ruby_llm:\n request_timeout: 180\n max_retries: 5\n log_level: :warn\n</code></pre> <p>ERB support</p> <p>Config files support ERB templating, so you can reference environment variables with <code>&lt;%= ENV['...'] %&gt;</code>. This is useful for keeping secrets out of config files while still using YAML structure.</p>"},{"location":"getting-started/configuration/#user-config","title":"User Config","text":"<p>Create <code>~/.config/robot_lab/config.yml</code> for personal defaults that apply across all your projects:</p> ~/.config/robot_lab/config.yml<pre><code>defaults:\n ruby_llm:\n anthropic_api_key: &lt;%= ENV['ANTHROPIC_API_KEY'] %&gt;\n model: claude-sonnet-4\n</code></pre>"},{"location":"getting-started/configuration/#configuration-reference","title":"Configuration Reference","text":""},{"location":"getting-started/configuration/#core-settings","title":"Core Settings","text":"Key Default Description <code>max_iterations</code> <code>10</code> Maximum robots per network run <code>max_tool_iterations</code> <code>10</code> Maximum tool calls per robot run <code>streaming_enabled</code> <code>true</code> Enable streaming by default <code>template_path</code> <code>null</code> (auto-detected) Directory for prompt templates <code>mcp</code> <code>:none</code> Global MCP server configuration <code>tools</code> <code>:none</code> Global tool whitelist"},{"location":"getting-started/configuration/#rubyllm-settings-ruby_llm-section","title":"RubyLLM Settings (<code>ruby_llm:</code> section)","text":"<p>All settings under the <code>ruby_llm:</code> key are applied to <code>RubyLLM.configure</code> automatically on startup.</p>"},{"location":"getting-started/configuration/#provider-api-keys","title":"Provider API Keys","text":"Key Description <code>ruby_llm.anthropic_api_key</code> Anthropic Claude API key <code>ruby_llm.openai_api_key</code> OpenAI API key <code>ruby_llm.gemini_api_key</code> Google Gemini API key <code>ruby_llm.deepseek_api_key</code> DeepSeek API key <code>ruby_llm.mistral_api_key</code> Mistral API key <code>ruby_llm.openrouter_api_key</code> OpenRouter API key <code>ruby_llm.bedrock_api_key</code> AWS Bedrock access key <code>ruby_llm.bedrock_secret_key</code> AWS Bedrock secret key <code>ruby_llm.bedrock_region</code> AWS Bedrock region <code>ruby_llm.xai_api_key</code> xAI (Grok) API key"},{"location":"getting-started/configuration/#model-defaults","title":"Model Defaults","text":"Key Default Description <code>ruby_llm.provider</code> <code>:anthropic</code> Default LLM provider <code>ruby_llm.model</code> <code>claude-sonnet-4</code> Default model for robots <code>ruby_llm.default_model</code> <code>null</code> RubyLLM default model override <code>ruby_llm.default_embedding_model</code> <code>null</code> Default embedding model <code>ruby_llm.default_image_model</code> <code>null</code> Default image model"},{"location":"getting-started/configuration/#connection-settings","title":"Connection Settings","text":"Key Default Description <code>ruby_llm.request_timeout</code> <code>120</code> Request timeout in seconds <code>ruby_llm.max_retries</code> <code>3</code> Maximum retry attempts <code>ruby_llm.retry_interval</code> <code>1</code> Seconds between retries <code>ruby_llm.retry_backoff_factor</code> <code>2</code> Exponential backoff factor <code>ruby_llm.http_proxy</code> <code>null</code> HTTP proxy URL"},{"location":"getting-started/configuration/#provider-endpoints-self-hosted-models","title":"Provider Endpoints (self-hosted models)","text":"Key Description <code>ruby_llm.openai_api_base</code> Custom OpenAI-compatible endpoint <code>ruby_llm.gemini_api_base</code> Custom Gemini endpoint <code>ruby_llm.ollama_api_base</code> Ollama endpoint (e.g., <code>http://localhost:11434</code>) <code>ruby_llm.gpustack_api_base</code> GPUStack endpoint"},{"location":"getting-started/configuration/#logging","title":"Logging","text":"Key Default Description <code>ruby_llm.log_file</code> <code>null</code> Path to log file <code>ruby_llm.log_level</code> <code>:info</code> Log level (<code>:debug</code>, <code>:info</code>, <code>:warn</code>, <code>:error</code>) <code>ruby_llm.log_stream_debug</code> <code>false</code> Log streaming debug output"},{"location":"getting-started/configuration/#chat-configuration-chat-section","title":"Chat Configuration (<code>chat:</code> section)","text":"<p>Default chat parameters applied to all robots unless overridden:</p> Key Default Description <code>chat.with_temperature</code> <code>0.7</code> Controls randomness (0.0-2.0) <code>chat.with_params.top_p</code> <code>null</code> Nucleus sampling threshold <code>chat.with_params.top_k</code> <code>null</code> Top-k sampling <code>chat.with_params.max_tokens</code> <code>null</code> Maximum tokens in response <code>chat.with_params.presence_penalty</code> <code>null</code> Presence penalty (-2.0 to 2.0) <code>chat.with_params.frequency_penalty</code> <code>null</code> Frequency penalty (-2.0 to 2.0) <code>chat.with_params.stop</code> <code>null</code> Stop sequences"},{"location":"getting-started/configuration/#runtime-only-attributes","title":"Runtime-Only Attributes","text":"<p>Some attributes can only be set at runtime, not through config files:</p> <pre><code># Logger (defaults to Rails.logger in Rails, or Logger.new($stdout) otherwise)\nRobotLab.config.logger = Logger.new(nil) # silence logging\nRobotLab.config.logger = Logger.new(\"robot.log\") # log to file\n</code></pre>"},{"location":"getting-started/configuration/#reloading-configuration","title":"Reloading Configuration","text":"<p>To reload configuration from all sources:</p> <pre><code>RobotLab.reload_config!\n</code></pre> <p>This clears the cached config and reloads from all sources on next access.</p>"},{"location":"getting-started/configuration/#environment-specific-configuration","title":"Environment-Specific Configuration","text":"<p>The <code>defaults.yml</code> shipped with RobotLab includes environment-specific overrides:</p> DevelopmentTestProduction <pre><code>development:\n ruby_llm:\n log_level: :debug\n</code></pre> <pre><code>test:\n max_iterations: 3\n streaming_enabled: false\n ruby_llm:\n model: claude-haiku-3-5\n request_timeout: 30\n max_retries: 1\n log_level: :warn\n</code></pre> <pre><code>production:\n streaming_enabled: false\n max_iterations: 20\n ruby_llm:\n request_timeout: 180\n max_retries: 5\n log_level: :warn\n</code></pre> <p>The current environment is determined automatically (via <code>RAILS_ENV</code>, <code>RACK_ENV</code>, or defaults to <code>development</code>).</p>"},{"location":"getting-started/configuration/#rails-integration","title":"Rails Integration","text":"<p>In Rails, RobotLab is configured automatically via its Railtie. The logger defaults to <code>Rails.logger</code>, and templates default to <code>app/prompts/</code>.</p> <p>Create a project config file for Rails-specific settings:</p> config/robot_lab.yml<pre><code>defaults:\n ruby_llm:\n anthropic_api_key: &lt;%= Rails.application.credentials.anthropic_api_key %&gt;\n model: claude-sonnet-4\n\n template_path: null # auto-detects app/prompts in Rails\n\nproduction:\n ruby_llm:\n request_timeout: 180\n max_retries: 5\n</code></pre> <p>You can also use Rails credentials:</p> <pre><code>rails credentials:edit\n</code></pre> <pre><code># config/credentials.yml.enc\nanthropic_api_key: sk-ant-...\nopenai_api_key: sk-...\n</code></pre> <p>Then reference them in your config file with ERB:</p> config/robot_lab.yml<pre><code>defaults:\n ruby_llm:\n anthropic_api_key: &lt;%= Rails.application.credentials.anthropic_api_key %&gt;\n</code></pre>"},{"location":"getting-started/configuration/#runconfig-shared-operational-defaults","title":"RunConfig: Shared Operational Defaults","text":"<p><code>RunConfig</code> is a configuration object that lets you express operational defaults for LLM settings, tools, callbacks, and infrastructure. Unlike <code>RobotLab.config</code> (which is global and static), RunConfig flows through the hierarchy and can be customized at each level:</p> <pre><code>RobotLab.config (global) -&gt; Network RunConfig -&gt; Robot RunConfig -&gt; Template front matter -&gt; Task RunConfig -&gt; Runtime\n</code></pre>"},{"location":"getting-started/configuration/#creating-a-runconfig","title":"Creating a RunConfig","text":"<pre><code># Keyword construction\nconfig = RobotLab::RunConfig.new(model: \"claude-sonnet-4\", temperature: 0.7)\n\n# Block DSL\nconfig = RobotLab::RunConfig.new do |c|\n c.model \"claude-sonnet-4\"\n c.temperature 0.7\n c.max_tokens 2000\nend\n\n# Chaining\nconfig = RobotLab::RunConfig.new\n .model(\"claude-sonnet-4\")\n .temperature(0.7)\n</code></pre>"},{"location":"getting-started/configuration/#applying-runconfig","title":"Applying RunConfig","text":"<p>Pass <code>config:</code> to robots and networks. Explicit constructor kwargs always override the RunConfig:</p> <pre><code># Shared config for a team of robots\nshared = RobotLab::RunConfig.new(model: \"claude-sonnet-4\", temperature: 0.5)\n\n# Robot uses shared config\nrobot = RobotLab.build(\n name: \"writer\",\n system_prompt: \"You are a creative writer.\",\n config: shared,\n temperature: 0.9 # overrides shared config's 0.5\n)\n\n# Network applies config to all member robots\nnetwork = RobotLab.create_network(name: \"pipeline\", config: shared) do\n task :analyzer, analyzer_robot, depends_on: :none\n task :writer, writer_robot, depends_on: [:analyzer]\nend\n</code></pre>"},{"location":"getting-started/configuration/#merging-configs","title":"Merging Configs","text":"<p>RunConfig supports merge semantics where the more-specific config's values win:</p> <pre><code>network_config = RobotLab::RunConfig.new(model: \"claude-sonnet-4\", temperature: 0.5)\nrobot_config = RobotLab::RunConfig.new(temperature: 0.9)\neffective = network_config.merge(robot_config)\neffective.model #=&gt; \"claude-sonnet-4\" (inherited)\neffective.temperature #=&gt; 0.9 (overridden)\n</code></pre>"},{"location":"getting-started/configuration/#available-fields","title":"Available Fields","text":"Category Fields LLM <code>model</code>, <code>temperature</code>, <code>top_p</code>, <code>top_k</code>, <code>max_tokens</code>, <code>presence_penalty</code>, <code>frequency_penalty</code>, <code>stop</code> Tools <code>mcp</code>, <code>tools</code> Callbacks <code>on_tool_call</code>, <code>on_tool_result</code> Infrastructure <code>bus</code>, <code>enable_cache</code>"},{"location":"getting-started/configuration/#runconfig-vs-robotlabconfig","title":"RunConfig vs RobotLab.config","text":"<code>RobotLab.config</code> <code>RunConfig</code> Scope Global (all robots) Per-network, per-robot, or per-task Source YAML files, env vars Code (constructor, block DSL) Mutability Loaded once, rarely changed Created per use case, merged Purpose API keys, timeouts, defaults Model, temperature, tools per workflow"},{"location":"getting-started/configuration/#robot-level-configuration","title":"Robot-Level Configuration","text":"<p>Individual robots can override the global model and other settings:</p> <pre><code># Override model for a specific robot\nrobot = RobotLab.build(\n name: \"fast_bot\",\n system_prompt: \"You are a quick responder.\",\n model: \"claude-haiku-3-5\",\n temperature: 0.3,\n max_tokens: 500\n)\n\n# Or use chaining at runtime\nrobot.with_temperature(0.9).with_max_tokens(2000).run(\"Tell me a story.\")\n</code></pre>"},{"location":"getting-started/configuration/#hierarchical-mcp-and-tools","title":"Hierarchical MCP and Tools","text":"<p>MCP servers and tools use a hierarchical configuration: <code>runtime &gt; robot &gt; network &gt; global</code>. Each level can specify:</p> <ul> <li><code>:inherit</code> -- Use the parent level's configuration</li> <li><code>:none</code> -- No MCP servers or tools at this level</li> <li>An explicit array -- Specific servers or tools</li> </ul> <pre><code># Robot inheriting network MCP config\nrobot = RobotLab.build(\n name: \"agent\",\n system_prompt: \"You are helpful.\",\n mcp: :inherit,\n tools: :inherit\n)\n\n# Robot with no MCP, specific tools\nrobot = RobotLab.build(\n name: \"calculator\",\n system_prompt: \"You solve math problems.\",\n mcp: :none,\n local_tools: [Calculator]\n)\n</code></pre>"},{"location":"getting-started/configuration/#next-steps","title":"Next Steps","text":"<ul> <li>Building Robots - Create custom robots</li> <li>Creating Networks - Network configuration</li> <li>MCP Integration - Configure MCP servers</li> </ul>"},{"location":"getting-started/installation/","title":"Installation","text":"<p>This guide covers installing RobotLab in your Ruby project.</p>"},{"location":"getting-started/installation/#requirements","title":"Requirements","text":"<ul> <li>Ruby: 3.2 or higher</li> <li>Bundler: 2.0 or higher (recommended)</li> </ul>"},{"location":"getting-started/installation/#install-via-bundler","title":"Install via Bundler","text":"<p>Add RobotLab to your <code>Gemfile</code>:</p> <pre><code>gem \"robot_lab\"\n</code></pre> <p>Then install:</p> <pre><code>bundle install\n</code></pre>"},{"location":"getting-started/installation/#install-via-rubygems","title":"Install via RubyGems","text":"<p>Or install directly:</p> <pre><code>gem install robot_lab\n</code></pre>"},{"location":"getting-started/installation/#dependencies","title":"Dependencies","text":"<p>RobotLab automatically installs these core dependencies:</p> Gem Purpose <code>ruby_llm</code> (~&gt; 1.12) LLM provider integrations (Anthropic, OpenAI, Gemini, etc.) <code>prompt_manager</code> (~&gt; 1.0) Template-based prompt management with YAML front matter <code>simple_flow</code> (~&gt; 0.3) Pipeline workflow execution for networks <code>myway_config</code> (~&gt; 0.1) Layered configuration (defaults, env vars, config files) <code>ruby_llm-mcp</code> Model Context Protocol client for external tool servers <code>ruby_llm-schema</code> Schema validation for structured outputs <code>ruby_llm-semantic_cache</code> Semantic caching for LLM responses <code>zeitwerk</code> (~&gt; 2.6) Autoloading and eager loading <code>async</code> (~&gt; 2.0) Fiber-based concurrency"},{"location":"getting-started/installation/#optional-dependencies","title":"Optional Dependencies","text":"<p>For specific features, you may need additional gems:</p> MCP WebSocket TransportMCP HTTP TransportRails Integration <pre><code>gem \"async-websocket\"\n</code></pre> <pre><code>gem \"async-http\"\n</code></pre> <pre><code># Rails is detected automatically\ngem \"rails\", \"&gt;= 7.0\"\n</code></pre>"},{"location":"getting-started/installation/#verify-installation","title":"Verify Installation","text":"<p>Create a test file to verify everything works:</p> <pre><code># test_robot_lab.rb\nrequire \"robot_lab\"\n\nputs \"RobotLab version: #{RobotLab::VERSION}\"\nputs \"Installation successful!\"\n</code></pre> <p>Run it:</p> <pre><code>ruby test_robot_lab.rb\n# =&gt; RobotLab version: 0.1.0\n# =&gt; Installation successful!\n</code></pre>"},{"location":"getting-started/installation/#rails-installation","title":"Rails Installation","text":"<p>For Rails applications, use the install generator:</p> <pre><code>rails generate robot_lab:install\n</code></pre> <p>This creates:</p> <ul> <li><code>config/initializers/robot_lab.rb</code> - Configuration file</li> <li><code>db/migrate/*_create_robot_lab_tables.rb</code> - Database migrations</li> <li><code>app/models/robot_lab_thread.rb</code> - Thread model</li> <li><code>app/models/robot_lab_result.rb</code> - Result model</li> <li><code>app/robots/</code> - Directory for robot definitions</li> <li><code>app/tools/</code> - Directory for tool definitions</li> </ul> <p>Then run migrations:</p> <pre><code>rails db:migrate\n</code></pre>"},{"location":"getting-started/installation/#environment-setup","title":"Environment Setup","text":"<p>RobotLab uses a layered configuration system (see Configuration for full details). The simplest way to get started is with environment variables:</p> Anthropic (Recommended)OpenAIGoogle Gemini <pre><code>export ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=\"sk-ant-...\"\n</code></pre> <pre><code>export ROBOT_LAB_RUBY_LLM__OPENAI_API_KEY=\"sk-...\"\n</code></pre> <pre><code>export ROBOT_LAB_RUBY_LLM__GEMINI_API_KEY=\"...\"\n</code></pre> <p>Using dotenv</p> <p>For development, consider using the dotenv gem to manage environment variables:</p> <pre><code># Gemfile\ngem \"dotenv\", groups: [:development, :test]\n</code></pre> <pre><code># .env\nROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...\n</code></pre> <p>Direct provider env vars</p> <p>RubyLLM also reads provider-specific environment variables directly (e.g., <code>ANTHROPIC_API_KEY</code>). If you already have those set, they will be picked up automatically. The <code>ROBOT_LAB_RUBY_LLM__*</code> prefix gives you explicit control through RobotLab's config layer.</p>"},{"location":"getting-started/installation/#troubleshooting","title":"Troubleshooting","text":""},{"location":"getting-started/installation/#gem-installation-fails","title":"Gem Installation Fails","text":"<p>If you encounter SSL or network errors:</p> <pre><code># Update RubyGems\ngem update --system\n\n# Try installing with verbose output\ngem install robot_lab --verbose\n</code></pre>"},{"location":"getting-started/installation/#missing-dependencies","title":"Missing Dependencies","text":"<p>If you see \"LoadError\" for optional gems:</p> <pre><code># Install the specific gem mentioned in the error\nbundle add async-websocket\n</code></pre>"},{"location":"getting-started/installation/#api-key-issues","title":"API Key Issues","text":"<p>If you see authentication errors:</p> <ol> <li>Verify your API key is set: <code>echo $ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY</code></li> <li>Check the key is valid in your provider's console</li> <li>Ensure you're using the correct environment variable name</li> </ol>"},{"location":"getting-started/installation/#next-steps","title":"Next Steps","text":"<p>Now that RobotLab is installed:</p> <ul> <li> Quick Start - Build your first robot</li> <li> Configuration - Configure defaults</li> </ul>"},{"location":"getting-started/quick-start/","title":"Quick Start","text":"<p>Build your first RobotLab application in 5 minutes.</p>"},{"location":"getting-started/quick-start/#step-1-set-up-api-keys","title":"Step 1: Set Up API Keys","text":"<p>RobotLab reads configuration from environment variables automatically. Set your API key before running any code:</p> <pre><code>export ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=\"sk-ant-...\"\n</code></pre> <p>Or create a config file at <code>./config/robot_lab.yml</code>:</p> <pre><code>defaults:\n ruby_llm:\n anthropic_api_key: &lt;%= ENV['ANTHROPIC_API_KEY'] %&gt;\n</code></pre> <p>See Configuration for all configuration options.</p>"},{"location":"getting-started/quick-start/#step-2-create-a-robot","title":"Step 2: Create a Robot","text":"<p>Build a simple assistant robot using keyword arguments:</p> <pre><code>require \"robot_lab\"\n\nassistant = RobotLab.build(\n name: \"assistant\",\n system_prompt: \"You are a helpful AI assistant. You provide clear, accurate, and concise answers.\"\n)\n</code></pre>"},{"location":"getting-started/quick-start/#step-3-run-it","title":"Step 3: Run It","text":"<p>Send a message and get a response:</p> <pre><code>result = assistant.run(\"What is Ruby on Rails?\")\n\nputs result.last_text_content\n</code></pre> <p>The <code>run</code> method takes a positional string message and returns a <code>RobotResult</code>. Use <code>last_text_content</code> to extract the response text.</p>"},{"location":"getting-started/quick-start/#complete-example","title":"Complete Example","text":"<p>Here is everything together in one file:</p> hello_robot.rb<pre><code>require \"robot_lab\"\n\n# Build a robot with an inline system prompt\nassistant = RobotLab.build(\n name: \"assistant\",\n system_prompt: \"You are a helpful AI assistant. Be concise and friendly.\"\n)\n\n# Run the robot with a message\nresult = assistant.run(\"Hello! What can you help me with?\")\n\n# Print the response\nputs result.last_text_content\n</code></pre> <p>Run it:</p> <pre><code>ruby hello_robot.rb\n</code></pre>"},{"location":"getting-started/quick-start/#using-templates","title":"Using Templates","text":"<p>Instead of inline prompts, you can use template files managed by <code>prompt_manager</code>. Templates are <code>.md</code> files with YAML front matter:</p> prompts/helper.md<pre><code>---\ndescription: A helpful assistant\nparameters:\n company_name: null\n---\nYou are a helpful assistant for &lt;%= company_name %&gt;.\nBe concise and friendly in your responses.\n</code></pre> <p>Create the robot with a template reference and context:</p> <pre><code>robot = RobotLab.build(\n name: \"helper\",\n template: :helper,\n context: { company_name: \"Acme Corp\" }\n)\n\nresult = robot.run(\"What services do you offer?\")\nputs result.last_text_content\n</code></pre> <p>Templates are loaded from the <code>prompts/</code> directory by default (or <code>app/prompts/</code> in Rails). You can change this in your config.</p>"},{"location":"getting-started/quick-start/#adding-a-tool","title":"Adding a Tool","text":"<p>Give your robot custom capabilities by defining a <code>RubyLLM::Tool</code> subclass:</p> hello_tools.rb<pre><code>require \"robot_lab\"\n\nclass CurrentTime &lt; RubyLLM::Tool\n description \"Get the current date and time\"\n\n param :timezone,\n type: \"string\",\n desc: \"Timezone name (e.g., 'UTC', 'US/Eastern')\",\n required: false\n\n def execute(timezone: \"UTC\")\n Time.now.getlocal(timezone_offset(timezone)).strftime(\"%Y-%m-%d %H:%M:%S %Z\")\n rescue =&gt; e\n Time.now.utc.strftime(\"%Y-%m-%d %H:%M:%S UTC\")\n end\n\n private\n\n def timezone_offset(tz)\n case tz\n when \"UTC\" then \"+00:00\"\n when \"US/Eastern\" then \"-05:00\"\n when \"US/Pacific\" then \"-08:00\"\n else \"+00:00\"\n end\n end\nend\n\n# Pass tools via the local_tools: parameter\nassistant = RobotLab.build(\n name: \"time_bot\",\n system_prompt: \"You are a helpful assistant. Use the current_time tool when users ask about the time.\",\n local_tools: [CurrentTime]\n)\n\nresult = assistant.run(\"What time is it right now?\")\nputs result.last_text_content\n</code></pre> <p>Tools are passed to the robot via the <code>local_tools:</code> keyword argument as an array of <code>RubyLLM::Tool</code> subclasses.</p>"},{"location":"getting-started/quick-start/#method-chaining","title":"Method Chaining","text":"<p>Robots support a chaining API for runtime adjustments:</p> <pre><code>robot = RobotLab.build(name: \"writer\")\n\nresult = robot\n .with_instructions(\"You are a creative fiction writer.\")\n .with_temperature(0.9)\n .with_model(\"claude-sonnet-4\")\n .run(\"Write a haiku about programming.\")\n\nputs result.last_text_content\n</code></pre> <p>Available chaining methods include <code>with_instructions</code>, <code>with_temperature</code>, <code>with_model</code>, <code>with_max_tokens</code>, <code>with_top_p</code>, <code>with_tools</code>, and more.</p>"},{"location":"getting-started/quick-start/#multi-robot-network","title":"Multi-Robot Network","text":"<p>Create a pipeline of robots using <code>RobotLab.create_network</code>. Networks use <code>SimpleFlow::Pipeline</code> under the hood with <code>task</code> definitions and dependency tracking:</p> hello_network.rb<pre><code>require \"robot_lab\"\n\n# Build specialized robots\nanalyst = RobotLab.build(\n name: \"analyst\",\n system_prompt: &lt;&lt;~PROMPT\n You are a text analyst. Analyze the given text and provide a brief\n summary of its key themes and sentiment. Be concise -- 2-3 sentences max.\n PROMPT\n)\n\nwriter = RobotLab.build(\n name: \"writer\",\n system_prompt: &lt;&lt;~PROMPT\n You are a professional copywriter. Based on the analysis you receive,\n write a short, engaging summary suitable for a newsletter. Keep it\n to one paragraph.\n PROMPT\n)\n\n# Create a sequential pipeline\nnetwork = RobotLab.create_network(name: \"content_pipeline\") do\n task :analyst, analyst, depends_on: :none\n task :writer, writer, depends_on: [:analyst]\nend\n\n# Run the network\nresult = network.run(\n message: \"Ruby 3.4 was released with significant performance improvements...\"\n)\n\n# The final result is from the last robot in the pipeline\nif result.value.is_a?(RobotLab::RobotResult)\n puts result.value.last_text_content\nend\n\n# Access intermediate results by task name\nif result.context[:analyst]\n puts \"\\nAnalysis: #{result.context[:analyst].last_text_content}\"\nend\n</code></pre>"},{"location":"getting-started/quick-start/#network-task-dependencies","title":"Network Task Dependencies","text":"<p>Tasks declare their dependencies to control execution order:</p> Dependency Meaning <code>depends_on: :none</code> Entry point -- runs first with no dependencies <code>depends_on: [:task_name]</code> Runs after the named task(s) complete <code>depends_on: :optional</code> Only runs if explicitly activated by a preceding task <p>Tasks with non-overlapping dependencies can execute in parallel automatically.</p>"},{"location":"getting-started/quick-start/#whats-next","title":"What's Next?","text":"<p>You have built your first RobotLab application. Here is where to go next:</p> <ul> <li> <p> Configuration</p> <p>Learn all configuration options</p> </li> <li> <p> Building Robots</p> <p>Deep dive into robot creation</p> </li> <li> <p> Using Tools</p> <p>Give robots custom capabilities</p> </li> <li> <p> Creating Networks</p> <p>Advanced network patterns</p> </li> </ul>"},{"location":"guides/","title":"Guides","text":"<p>Practical guides for building applications with RobotLab.</p>"},{"location":"guides/#getting-started","title":"Getting Started","text":"<p>If you're new to RobotLab, start here:</p> <ul> <li> <p> Building Robots</p> <p>Create specialized AI agents with personalities and tools</p> </li> <li> <p> Creating Networks</p> <p>Orchestrate multiple robots for complex workflows</p> </li> </ul>"},{"location":"guides/#core-features","title":"Core Features","text":"<ul> <li> <p> Using Tools</p> <p>Give robots custom capabilities to interact with external systems</p> </li> <li> <p> MCP Integration</p> <p>Connect to Model Context Protocol servers</p> </li> <li> <p> Streaming Responses</p> <p>Real-time streaming of LLM responses</p> </li> <li> <p> Memory System</p> <p>Share data between robots with the memory system</p> </li> </ul>"},{"location":"guides/#framework-integration","title":"Framework Integration","text":"<ul> <li> <p> Rails Integration</p> <p>Use RobotLab in Ruby on Rails applications</p> </li> </ul>"},{"location":"guides/#guide-index","title":"Guide Index","text":"Guide Description Time Building Robots Create and configure robots 10 min Creating Networks Multi-robot orchestration 15 min Using Tools Add custom capabilities 10 min MCP Integration External tool servers 10 min Streaming Real-time responses 5 min Memory Shared data store 5 min Rails Integration Rails application setup 15 min"},{"location":"guides/building-robots/","title":"Building Robots","text":"<p>This guide covers everything you need to know about creating robots in RobotLab.</p>"},{"location":"guides/building-robots/#basic-robot","title":"Basic Robot","text":"<p>Create a robot using the <code>RobotLab.build</code> factory method with keyword arguments:</p> <pre><code>robot = RobotLab.build(\n name: \"assistant\",\n system_prompt: \"You are a helpful assistant.\"\n)\n\nresult = robot.run(\"Hello!\")\nputs result.last_text_content\n</code></pre>"},{"location":"guides/building-robots/#robot-properties","title":"Robot Properties","text":""},{"location":"guides/building-robots/#name","title":"Name","text":"<p>A unique identifier used for routing and logging. If omitted, an auto-generated name is used:</p> <pre><code>robot = RobotLab.build(name: \"support_agent\", system_prompt: \"...\")\n</code></pre>"},{"location":"guides/building-robots/#description","title":"Description","text":"<p>Describes what the robot does (useful for routing decisions):</p> <pre><code>robot = RobotLab.build(\n name: \"support_agent\",\n description: \"Handles customer support inquiries about orders and refunds\",\n system_prompt: \"...\"\n)\n</code></pre>"},{"location":"guides/building-robots/#model","title":"Model","text":"<p>The LLM model to use. Defaults to the value in <code>RobotLab.config.ruby_llm.model</code>:</p> <pre><code>robot = RobotLab.build(\n name: \"writer\",\n model: \"claude-sonnet-4\",\n system_prompt: \"You are a creative writer.\"\n)\n</code></pre>"},{"location":"guides/building-robots/#system-prompt","title":"System Prompt","text":"<p>An inline string that defines the robot's personality and behavior:</p> <pre><code>robot = RobotLab.build(\n name: \"support\",\n system_prompt: &lt;&lt;~PROMPT\n You are a customer support specialist for TechCo.\n\n Your responsibilities:\n - Answer questions about products and services\n - Help resolve order issues\n - Provide friendly, professional assistance\n\n Always be polite and acknowledge the customer's concerns.\n PROMPT\n)\n</code></pre>"},{"location":"guides/building-robots/#template-files","title":"Template Files","text":"<p>Templates are <code>.md</code> files managed by prompt_manager. Reference a template by symbol; RobotLab resolves it through the configured template path.</p> <pre><code># Reference template by symbol (loads prompts/support.md)\nrobot = RobotLab.build(\n name: \"support\",\n template: :support,\n context: { company: \"TechCo\", tone: \"friendly\" }\n)\n</code></pre>"},{"location":"guides/building-robots/#template-format","title":"Template Format","text":"<p>Templates use <code>.md</code> files with YAML front matter:</p> prompts/support.md<pre><code>---\ndescription: Customer support assistant\nparameters:\n company: \"Acme\"\n tone: \"professional\"\nmodel: claude-sonnet-4\ntemperature: 0.7\n---\nYou are a support agent for &lt;%= company %&gt;.\nYour tone should be &lt;%= tone %&gt;.\n</code></pre>"},{"location":"guides/building-robots/#front-matter-configuration","title":"Front Matter Configuration","text":"<p>The following YAML front matter keys are applied to the robot's chat automatically:</p> <p>LLM Configuration:</p> Key Description <code>model</code> Override the LLM model <code>temperature</code> Controls randomness (0.0 - 1.0) <code>top_p</code> Nucleus sampling threshold <code>top_k</code> Top-k sampling <code>max_tokens</code> Maximum tokens in response <code>presence_penalty</code> Penalize based on presence <code>frequency_penalty</code> Penalize based on frequency <code>stop</code> Stop sequences <p>Robot Identity and Capabilities:</p> Key Description <code>robot_name</code> Override the robot's name (when constructor uses the default) <code>description</code> Human-readable description of the robot <code>tools</code> Array of tool class names (resolved via <code>Object.const_get</code>) <code>mcp</code> Array of MCP server configurations <code>skills</code> Array of skill template symbols to prepend (see Composable Skills) <p>Constructor-provided values always take precedence over frontmatter values.</p>"},{"location":"guides/building-robots/#self-contained-templates","title":"Self-Contained Templates","text":"<p>Templates can declare everything a robot needs \u2014 identity, tools, MCP servers, and LLM config \u2014 making the <code>.md</code> file a complete robot definition:</p> prompts/github_assistant.md<pre><code>---\ndescription: GitHub assistant with MCP tool access\nrobot_name: github_bot\nmcp:\n - name: github\n transport: stdio\n command: npx\n args: [\"-y\", \"@modelcontextprotocol/server-github\"]\nmodel: claude-sonnet-4\ntemperature: 0.3\n---\nYou are a helpful GitHub assistant with access to GitHub tools via MCP.\nUse the available tools to help answer questions about GitHub repositories.\n</code></pre> <p>Build the robot with minimal constructor arguments:</p> <pre><code># Template provides name, description, MCP config, model, and temperature\nrobot = RobotLab.build(template: :github_assistant)\n</code></pre>"},{"location":"guides/building-robots/#tools-in-front-matter","title":"Tools in Front Matter","text":"<p>Declare tool classes by name in the <code>tools:</code> key. RobotLab resolves each string to a Ruby constant and instantiates it:</p> prompts/order_support.md<pre><code>---\ndescription: Order support specialist\ntools:\n - OrderLookup\n - RefundProcessor\n---\nYou help customers with order inquiries and refunds.\n</code></pre> <pre><code># Tools are loaded from frontmatter \u2014 no local_tools: needed\nrobot = RobotLab.build(template: :order_support)\n</code></pre> <p>Tool classes must be defined and loaded before the robot is built. If a tool name cannot be resolved, it is skipped with a warning.</p> <p>Constructor <code>local_tools:</code> overrides frontmatter <code>tools:</code> when provided:</p> <pre><code># Constructor tools take precedence over frontmatter tools\nrobot = RobotLab.build(\n template: :order_support,\n local_tools: [OrderLookup] # Only OrderLookup, not RefundProcessor\n)\n</code></pre>"},{"location":"guides/building-robots/#mcp-in-front-matter","title":"MCP in Front Matter","text":"<p>Declare MCP server configurations directly in the template:</p> prompts/developer.md<pre><code>---\ndescription: Developer assistant with filesystem access\nmcp:\n - name: filesystem\n transport: stdio\n command: mcp-server-filesystem\n args: [\"--root\", \"/home/user/projects\"]\n---\nYou are a developer assistant with filesystem access.\n</code></pre> <pre><code>robot = RobotLab.build(template: :developer)\n</code></pre> <p>Constructor <code>mcp:</code> overrides frontmatter <code>mcp:</code> when provided.</p>"},{"location":"guides/building-robots/#template-with-system-prompt","title":"Template with System Prompt","text":"<p>You can combine a template and an inline system prompt. Both are applied to the chat -- the template first, then the system prompt is appended as additional instructions:</p> <pre><code>robot = RobotLab.build(\n name: \"support\",\n template: :support,\n context: { company: \"TechCo\" },\n system_prompt: \"Always respond in Spanish.\"\n)\n</code></pre>"},{"location":"guides/building-robots/#composable-skills","title":"Composable Skills","text":"<p>Skills let you compose robot behaviors from reusable templates without creating a dedicated template for every combination. A skill is just a regular template whose prompt body gets prepended before the main template's body.</p>"},{"location":"guides/building-robots/#why-skills","title":"Why Skills?","text":"<p>Consider a support agent that needs to:</p> <ul> <li>Ask clarifying questions before acting</li> <li>Detect customer sentiment</li> <li>Respond in structured JSON</li> </ul> <p>Without skills, you'd create a single monolithic template or copy-paste shared instructions across templates. With skills, each behavior is a standalone template that can be mixed into any robot.</p>"},{"location":"guides/building-robots/#defining-a-skill","title":"Defining a Skill","text":"<p>A skill is a standard <code>.md</code> template file. There is no special syntax \u2014 any template can be used as a skill:</p> prompts/clarifier.md<pre><code>---\ndescription: Ask clarifying questions before acting\n---\nBefore answering, consider whether the user's request is ambiguous.\nIf so, ask one focused clarifying question before proceeding.\n</code></pre> prompts/json_responder.md<pre><code>---\ndescription: Respond in structured JSON\ntemperature: 0.2\n---\nAlways respond with valid JSON. Use this structure:\n{\"answer\": \"...\", \"confidence\": 0.0-1.0, \"sources\": [...]}\n</code></pre>"},{"location":"guides/building-robots/#using-skills-via-constructor","title":"Using Skills via Constructor","text":"<p>Pass <code>skills:</code> as a symbol or array of symbols:</p> <pre><code># Single skill\nrobot = RobotLab.build(\n name: \"bot\",\n template: :support,\n skills: :clarifier\n)\n\n# Multiple skills\nrobot = RobotLab.build(\n name: \"bot\",\n template: :support,\n skills: [:clarifier, :json_responder],\n context: { company: \"Acme Corp\" }\n)\n</code></pre> <p>The resulting system prompt is composed in order: clarifier body, then json_responder body, then the main support template body.</p>"},{"location":"guides/building-robots/#using-skills-via-front-matter","title":"Using Skills via Front Matter","text":"<p>Templates can declare skills directly in their front matter:</p> prompts/smart_support.md<pre><code>---\ndescription: Support agent with built-in skills\nskills:\n - clarifier\n - json_responder\nparameters:\n company: null\n---\nYou are a support agent for &lt;%= company %&gt;.\nHelp customers with their inquiries.\n</code></pre> <pre><code># Skills are loaded from front matter automatically\nrobot = RobotLab.build(\n template: :smart_support,\n context: { company: \"Acme Corp\" }\n)\n</code></pre> <p>Constructor <code>skills:</code> and front matter <code>skills:</code> are combined \u2014 constructor skills are processed first, then front matter skills.</p>"},{"location":"guides/building-robots/#nested-skills","title":"Nested Skills","text":"<p>Skills can reference other skills, enabling layered composition:</p> prompts/safety.md<pre><code>---\ndescription: Safety guidelines\nskills:\n - content_filter\n - pii_redactor\n---\nFollow all safety guidelines when responding.\n</code></pre> <p>Nested skills are expanded depth-first. For the example above, the prompt order would be: content_filter, pii_redactor, safety, then the main template.</p>"},{"location":"guides/building-robots/#cycle-detection","title":"Cycle Detection","text":"<p>If skills form a cycle (A references B, B references A), RobotLab detects it automatically, logs a warning, and skips the duplicate. This prevents infinite loops.</p>"},{"location":"guides/building-robots/#config-cascade","title":"Config Cascade","text":"<p>Skills can include LLM configuration in their front matter. Config cascades in processing order \u2014 later values override earlier ones:</p> prompts/creative_mode.md<pre><code>---\ndescription: Enable creative responses\ntemperature: 0.9\ntop_p: 0.95\n---\nBe creative and imaginative in your responses.\n</code></pre> <pre><code>robot = RobotLab.build(\n name: \"writer\",\n template: :article_writer,\n skills: [:creative_mode]\n)\n# temperature is 0.9 from the skill (unless the main template or constructor overrides it)\n</code></pre> <p>The precedence order (highest wins):</p> <ol> <li>Constructor kwargs (<code>temperature: 0.3</code>)</li> <li>Main template front matter</li> <li>Later skills override earlier skills</li> <li>First skill in the list</li> </ol>"},{"location":"guides/building-robots/#skills-without-a-main-template","title":"Skills Without a Main Template","text":"<p>Skills work without a main template \u2014 useful for quick composition:</p> <pre><code>robot = RobotLab.build(\n name: \"safe_bot\",\n skills: [:safety, :json_responder],\n system_prompt: \"You answer questions about our product.\"\n)\n</code></pre>"},{"location":"guides/building-robots/#shared-context","title":"Shared Context","text":"<p>All skills and the main template render with the same <code>context:</code> hash. Define parameters in each skill's front matter and pass values through the shared context:</p> prompts/branded.md<pre><code>---\ndescription: Brand-aware responses\nparameters:\n company_name: null\n---\nYou represent &lt;%= company_name %&gt;. Always maintain brand voice.\n</code></pre> <pre><code>robot = RobotLab.build(\n template: :support,\n skills: [:branded],\n context: { company_name: \"Acme Corp\" } # shared with all skills\n)\n</code></pre>"},{"location":"guides/building-robots/#adding-tools","title":"Adding Tools","text":"<p>Give robots capabilities via the <code>local_tools:</code> parameter. Tools can be <code>RubyLLM::Tool</code> subclasses or <code>RobotLab::Tool</code> instances:</p> <pre><code>robot = RobotLab.build(\n name: \"order_assistant\",\n system_prompt: \"You help customers with orders.\",\n local_tools: [OrderLookup, InventoryCheck]\n)\n</code></pre> <p>See the Using Tools guide for details on defining tools.</p>"},{"location":"guides/building-robots/#mcp-configuration","title":"MCP Configuration","text":"<p>Connect to MCP (Model Context Protocol) servers via the <code>mcp:</code> parameter:</p> <pre><code>robot = RobotLab.build(\n name: \"coder\",\n template: :developer,\n mcp: [\n {\n name: \"filesystem\",\n transport: { type: \"stdio\", command: \"mcp-server-fs\", args: [\"--root\", \"/data\"] }\n }\n ]\n)\n</code></pre> <p>MCP configuration supports hierarchical resolution:</p> Value Behavior <code>:none</code> No MCP servers (default) <code>:inherit</code> Use parent network/config MCP servers <code>[...]</code> Explicit array of server configurations <p>See the MCP Integration guide for transport types and advanced patterns.</p>"},{"location":"guides/building-robots/#chaining-configuration","title":"Chaining Configuration","text":"<p>Robots support <code>with_*</code> method chaining for runtime reconfiguration. Each method returns <code>self</code> for fluent usage:</p> <pre><code>robot = RobotLab.build(name: \"bot\")\n\nresult = robot\n .with_instructions(\"Be concise and direct.\")\n .with_temperature(0.9)\n .with_model(\"claude-sonnet-4\")\n .run(\"Summarize quantum computing in one sentence.\")\n</code></pre>"},{"location":"guides/building-robots/#available-chain-methods","title":"Available Chain Methods","text":"Method Description <code>with_model(id)</code> Change the LLM model <code>with_instructions(text)</code> Set system instructions <code>with_temperature(val)</code> Set temperature <code>with_top_p(val)</code> Set nucleus sampling <code>with_top_k(val)</code> Set top-k sampling <code>with_max_tokens(val)</code> Set max output tokens <code>with_presence_penalty(val)</code> Set presence penalty <code>with_frequency_penalty(val)</code> Set frequency penalty <code>with_stop(sequences)</code> Set stop sequences <code>with_tool(tool)</code> Add a single tool <code>with_tools(*tools)</code> Add multiple tools <code>with_template(id, **ctx)</code> Apply a prompt template <code>with_schema(schema)</code> Set structured output schema <code>with_thinking(config)</code> Enable extended thinking <code>with_bus(bus)</code> Connect to a message bus (creates one if nil)"},{"location":"guides/building-robots/#running-robots","title":"Running Robots","text":""},{"location":"guides/building-robots/#standalone","title":"Standalone","text":"<p>Run a robot directly with a string message:</p> <pre><code>result = robot.run(\"Hello!\")\nputs result.last_text_content\n</code></pre> <p>The <code>run</code> method returns a <code>RobotResult</code> with:</p> <pre><code>result.last_text_content # =&gt; \"Hi there! How can I help?\"\nresult.output # =&gt; Array of output messages\nresult.tool_calls # =&gt; Array of tool call results\nresult.robot_name # =&gt; \"assistant\"\nresult.stop_reason # =&gt; stop reason from the LLM\n</code></pre>"},{"location":"guides/building-robots/#with-runtime-memory","title":"With Runtime Memory","text":"<p>Inject memory values for a single run:</p> <pre><code>result = robot.run(\"What's my account status?\", memory: { user_id: 123 })\n</code></pre>"},{"location":"guides/building-robots/#in-a-network","title":"In a Network","text":"<p>Run through a network for orchestration:</p> <pre><code>network = RobotLab.create_network(name: \"pipeline\") do\n task :assistant, robot, depends_on: :none\nend\n\nresult = network.run(message: \"Hello!\")\nputs result.value.last_text_content\n</code></pre>"},{"location":"guides/building-robots/#with-streaming","title":"With Streaming","text":"<p>Stream LLM content in real-time using a stored callback, a per-call block, or both. Each receives a <code>RubyLLM::Chunk</code> object \u2014 use <code>chunk.content</code> for the text delta. Chunks also carry <code>model_id</code>, <code>tool_calls</code>, <code>thinking</code>, and token usage on the final chunk. See the Streaming API reference for the full chunk interface.</p> <p>Stored callback \u2014 wired at build time, fires on every <code>run()</code>:</p> <pre><code>robot = RobotLab.build(\n name: \"assistant\",\n system_prompt: \"You are helpful.\",\n on_content: -&gt;(chunk) { print chunk.content }\n)\nrobot.run(\"Tell me a story\") # streams automatically\n</code></pre> <p>Per-call block \u2014 passed to <code>run()</code>:</p> <pre><code>robot.run(\"Tell me a story\") { |chunk| print chunk.content }\n</code></pre> <p>Both together \u2014 stored fires first, then the block:</p> <pre><code>robot = RobotLab.build(\n name: \"assistant\",\n system_prompt: \"You are helpful.\",\n on_content: -&gt;(chunk) { log_chunk(chunk.content) }\n)\nrobot.run(\"Tell me a story\") { |chunk| stream_to_client(chunk.content) }\n</code></pre> <p>The <code>on_content</code> callback participates in the RunConfig cascade, so it can be set at the config level and inherited by robots:</p> <pre><code>config = RobotLab::RunConfig.new(\n on_content: -&gt;(chunk) { broadcast(chunk.content) }\n)\nrobot = RobotLab.build(name: \"bot\", system_prompt: \"...\", config: config)\n</code></pre> <p>You can also monitor tool activity via callbacks:</p> <pre><code>robot = RobotLab.build(\n name: \"assistant\",\n system_prompt: \"...\",\n on_tool_call: -&gt;(tool_call) { puts \"Calling: #{tool_call.name}\" },\n on_tool_result: -&gt;(result) { puts \"Result: #{result}\" }\n)\n</code></pre>"},{"location":"guides/building-robots/#robot-patterns","title":"Robot Patterns","text":""},{"location":"guides/building-robots/#classifier-robot","title":"Classifier Robot","text":"<p>Route requests to specialized handlers. Subclass <code>RobotLab::Robot</code> and override <code>call</code> for custom pipeline behavior:</p> <pre><code>class ClassifierRobot &lt; RobotLab::Robot\n def call(result)\n context = extract_run_context(result)\n message = context.delete(:message)\n robot_result = run(message, **context)\n\n new_result = result\n .with_context(@name.to_sym, robot_result)\n .continue(robot_result)\n\n category = robot_result.last_text_content.to_s.strip.downcase\n\n case category\n when /billing/ then new_result.activate(:billing)\n when /technical/ then new_result.activate(:technical)\n else new_result.activate(:general)\n end\n end\nend\n\nclassifier = ClassifierRobot.new(\n name: \"classifier\",\n system_prompt: &lt;&lt;~PROMPT\n Classify the user's message into exactly one category:\n - billing\n - technical\n - general\n Respond with only the category name, nothing else.\n PROMPT\n)\n</code></pre>"},{"location":"guides/building-robots/#specialist-robot","title":"Specialist Robot","text":"<p>Handle specific domains with template and tools:</p> <pre><code>billing_specialist = RobotLab.build(\n name: \"billing_specialist\",\n description: \"Handles billing and payment inquiries\",\n template: :billing,\n context: { department: \"billing\" },\n local_tools: [InvoiceLookup, RefundProcessor]\n)\n</code></pre>"},{"location":"guides/building-robots/#summarizer-robot","title":"Summarizer Robot","text":"<p>Condense information:</p> <pre><code>summarizer = RobotLab.build(\n name: \"summarizer\",\n description: \"Summarizes conversations and documents\",\n system_prompt: &lt;&lt;~PROMPT\n Create concise summaries of the provided content.\n Focus on key points and actionable items.\n Use bullet points for clarity.\n PROMPT\n)\n</code></pre>"},{"location":"guides/building-robots/#bus-connected-robot","title":"Bus-Connected Robot","text":"<p>Enable bidirectional communication between robots using a message bus. This pattern supports negotiation loops and convergence:</p> <pre><code>bus = TypedBus::MessageBus.new\n\nclass Comedian &lt; RobotLab::Robot\n def initialize(bus:)\n super(name: \"bob\", template: :comedian, bus: bus)\n on_message do |message|\n joke = run(message.content.to_s).last_text_content.strip\n send_reply(to: message.from.to_sym, content: joke, in_reply_to: message.key)\n end\n end\nend\n\nclass ComedyCritic &lt; RobotLab::Robot\n def initialize(bus:)\n super(name: \"alice\", template: :comedy_critic, bus: bus)\n @accepted = false\n on_message do |message|\n verdict = run(\"Evaluate: #{message.content}\").last_text_content.strip\n @accepted = verdict.start_with?(\"FUNNY\")\n send_message(to: :bob, content: \"Try again.\") unless @accepted\n end\n end\n attr_reader :accepted\nend\n\nbob = Comedian.new(bus: bus)\nalice = ComedyCritic.new(bus: bus)\nalice.send_message(to: :bob, content: \"Tell me a funny robot joke.\")\n</code></pre> <p>The <code>on_message</code> block arity controls delivery handling: - 1 argument <code>|message|</code> \u2014 auto-acknowledges before calling - 2 arguments <code>|delivery, message|</code> \u2014 manual <code>delivery.ack!</code> / <code>delivery.nack!</code></p> <p>See Message Bus for details.</p>"},{"location":"guides/building-robots/#spawning-robots-dynamically","title":"Spawning Robots Dynamically","text":"<p>Create new robots at runtime using <code>spawn</code>. The bus is created lazily \u2014 no upfront wiring required:</p> <pre><code>class Dispatcher &lt; RobotLab::Robot\n attr_reader :spawned\n\n def initialize(bus: nil)\n super(name: \"dispatcher\", template: :dispatcher, bus: bus)\n @spawned = {}\n\n on_message do |message|\n puts \"#{message.from} replied: #{message.content.to_s.lines.first&amp;.strip}\"\n end\n end\n\n def dispatch(question)\n # Ask LLM what specialist to create\n plan = run(question).last_text_content.strip\n role, instruction = plan.split(\"\\n\", 2)\n role = role.strip.downcase.gsub(/\\s+/, \"_\")\n\n # Spawn (or reuse) a specialist\n specialist = @spawned[role] ||= spawn(\n name: role,\n system_prompt: instruction&amp;.strip || \"You are a helpful #{role}.\"\n )\n\n # Have the specialist answer and reply\n answer = specialist.run(question).last_text_content.strip\n specialist.send_message(to: :dispatcher, content: answer)\n end\nend\n</code></pre> <p>Key features of <code>spawn</code>:</p> <ul> <li>Creates a child robot on the same bus as the parent</li> <li>Creates a bus lazily if the parent doesn't have one</li> <li>Spawned robots can immediately send and receive messages</li> <li>Multiple robots with the same name enable fan-out messaging</li> </ul> <p>Robots can also join a bus after creation:</p> <pre><code>bot = RobotLab.build(name: \"latecomer\", system_prompt: \"Hello.\")\nbot.with_bus(existing_bus) # now connected and can send/receive messages\n</code></pre>"},{"location":"guides/building-robots/#configuration","title":"Configuration","text":"<p>RobotLab uses <code>MywayConfig</code> for configuration. Access the config object directly -- there is no <code>RobotLab.configure</code> block:</p> <pre><code>RobotLab.config.ruby_llm.model # =&gt; \"claude-sonnet-4\"\nRobotLab.config.ruby_llm.request_timeout # =&gt; 120\n</code></pre> <p>Configuration is loaded from:</p> <ul> <li>Bundled defaults (<code>lib/robot_lab/config/defaults.yml</code>)</li> <li>Environment-specific overrides (development, test, production)</li> <li>XDG config files (<code>~/.config/robot_lab/config.yml</code>)</li> <li>Project config (<code>./config/robot_lab.yml</code>)</li> <li>Environment variables (<code>ROBOT_LAB_*</code> prefix)</li> </ul>"},{"location":"guides/building-robots/#best-practices","title":"Best Practices","text":""},{"location":"guides/building-robots/#1-clear-focused-prompts","title":"1. Clear, Focused Prompts","text":"<pre><code># Good: Specific and focused\nrobot = RobotLab.build(\n name: \"reviewer\",\n system_prompt: &lt;&lt;~PROMPT\n You are a code reviewer. Review code for:\n - Security vulnerabilities\n - Performance issues\n - Best practice violations\n\n Provide specific line numbers and suggestions.\n PROMPT\n)\n\n# Bad: Vague and unfocused\nrobot = RobotLab.build(\n name: \"reviewer\",\n system_prompt: \"You help with code stuff.\"\n)\n</code></pre>"},{"location":"guides/building-robots/#2-compose-behaviors-with-skills","title":"2. Compose Behaviors with Skills","text":"<p>Instead of creating monolithic templates, break behaviors into composable skills:</p> <pre><code>robot = RobotLab.build(\n name: \"support\",\n template: :support,\n skills: [:clarifier, :safety, :json_responder]\n)\n</code></pre>"},{"location":"guides/building-robots/#3-use-templates-for-reusable-prompts","title":"3. Use Templates for Reusable Prompts","text":"<p>Templates keep prompts in version-controlled files and allow parameterization:</p> <pre><code>robot = RobotLab.build(\n name: \"support\",\n template: :support,\n context: { company: \"TechCo\", language: \"English\" }\n)\n</code></pre>"},{"location":"guides/building-robots/#4-handle-tool-errors-gracefully","title":"4. Handle Tool Errors Gracefully","text":"<p><code>RobotLab::Tool</code> automatically catches exceptions and returns plain-text errors to the LLM. For domain-specific error handling, catch known exceptions in <code>execute</code> and return structured data. See Using Tools: Error Handling for details.</p>"},{"location":"guides/building-robots/#next-steps","title":"Next Steps","text":"<ul> <li>Creating Networks - Orchestrate multiple robots</li> <li>Message Bus - Bidirectional robot communication</li> <li>Dynamic Spawning - Robots creating robots at runtime</li> <li>Using Tools - Advanced tool patterns</li> <li>Memory Guide - Share data between runs and robots</li> <li>API Reference: Robot - Complete API documentation</li> </ul>"},{"location":"guides/creating-networks/","title":"Creating Networks","text":"<p>Networks orchestrate multiple robots using SimpleFlow pipelines with DAG-based execution and optional task activation.</p>"},{"location":"guides/creating-networks/#basic-network","title":"Basic Network","text":"<p>Create a network with a sequential pipeline:</p> <pre><code>network = RobotLab.create_network(name: \"pipeline\") do\n task :analyzer, analyzer_robot, depends_on: :none\n task :writer, writer_robot, depends_on: [:analyzer]\n task :reviewer, reviewer_robot, depends_on: [:writer]\nend\n\nresult = network.run(message: \"Analyze this document\")\n</code></pre>"},{"location":"guides/creating-networks/#network-properties","title":"Network Properties","text":""},{"location":"guides/creating-networks/#name","title":"Name","text":"<p>Identifies the network for logging and debugging:</p> <pre><code>network = RobotLab.create_network(name: \"customer_service\") do\n # ...\nend\n</code></pre>"},{"location":"guides/creating-networks/#concurrency","title":"Concurrency","text":"<p>Control parallel execution mode:</p> <pre><code>network = RobotLab.create_network(name: \"parallel\", concurrency: :threads) do\n # :auto (default), :threads, or :async\nend\n</code></pre>"},{"location":"guides/creating-networks/#shared-memory","title":"Shared Memory","text":"<p>Networks provide a shared memory accessible to all robots:</p> <pre><code>network = RobotLab.create_network(name: \"pipeline\") do\n task :first, robot1, depends_on: :none\nend\n\n# Pre-populate shared memory\nnetwork.memory[:project] = \"Q4 Report\"\nnetwork.memory[:user_id] = 123\n</code></pre>"},{"location":"guides/creating-networks/#adding-tasks","title":"Adding Tasks","text":""},{"location":"guides/creating-networks/#sequential-tasks","title":"Sequential Tasks","text":"<p>Each task depends on the previous:</p> <pre><code>network = RobotLab.create_network(name: \"pipeline\") do\n task :first, robot1, depends_on: :none\n task :second, robot2, depends_on: [:first]\n task :third, robot3, depends_on: [:second]\nend\n</code></pre>"},{"location":"guides/creating-networks/#parallel-tasks","title":"Parallel Tasks","text":"<p>Tasks with the same dependencies run in parallel:</p> <pre><code>network = RobotLab.create_network(name: \"parallel_analysis\") do\n task :fetch, fetcher, depends_on: :none\n\n # These run in parallel after :fetch\n task :sentiment, sentiment_bot, depends_on: [:fetch]\n task :entities, entity_bot, depends_on: [:fetch]\n task :keywords, keyword_bot, depends_on: [:fetch]\n\n # This waits for all three to complete\n task :merge, merger, depends_on: [:sentiment, :entities, :keywords]\nend\n</code></pre>"},{"location":"guides/creating-networks/#optional-tasks","title":"Optional Tasks","text":"<p>Optional tasks only run when explicitly activated by a preceding robot:</p> <pre><code>network = RobotLab.create_network(name: \"router\") do\n task :classifier, classifier_robot, depends_on: :none\n task :billing, billing_robot, depends_on: :optional\n task :technical, technical_robot, depends_on: :optional\n task :general, general_robot, depends_on: :optional\nend\n</code></pre>"},{"location":"guides/creating-networks/#per-task-configuration","title":"Per-Task Configuration","text":"<p>Tasks can have individual context and configuration that is deep-merged with the network's run parameters:</p> <pre><code>network = RobotLab.create_network(name: \"support\") do\n task :classifier, classifier_robot, depends_on: :none\n task :billing, billing_robot,\n context: { department: \"billing\", escalation_level: 2 },\n depends_on: :optional\n task :technical, technical_robot,\n context: { department: \"technical\" },\n tools: [DebugTool, LogTool],\n depends_on: :optional\nend\n</code></pre>"},{"location":"guides/creating-networks/#task-options","title":"Task Options","text":"Option Description <code>context</code> Hash merged with run params (task values override) <code>mcp</code> MCP servers for this task (<code>:none</code>, <code>:inherit</code>, or array) <code>tools</code> Tools available to this task (<code>:none</code>, <code>:inherit</code>, or array) <code>memory</code> Task-specific memory <code>config</code> Per-task <code>RunConfig</code> (merged on top of network's config) <code>depends_on</code> <code>:none</code>, <code>[:task1]</code>, or <code>:optional</code>"},{"location":"guides/creating-networks/#conditional-routing","title":"Conditional Routing","text":"<p>Use optional tasks with custom Robot subclasses for intelligent routing:</p> <pre><code>class ClassifierRobot &lt; RobotLab::Robot\n def call(result)\n context = extract_run_context(result)\n message = context.delete(:message)\n robot_result = run(message, **context)\n\n new_result = result\n .with_context(@name.to_sym, robot_result)\n .continue(robot_result)\n\n # Activate appropriate specialist based on classification\n category = robot_result.last_text_content.to_s.strip.downcase\n\n case category\n when /billing/ then new_result.activate(:billing)\n when /technical/ then new_result.activate(:technical)\n else new_result.activate(:general)\n end\n end\nend\n\nclassifier = ClassifierRobot.new(\n name: \"classifier\",\n system_prompt: \"Classify as: billing, technical, or general. Respond with one word.\"\n)\n\nnetwork = RobotLab.create_network(name: \"support\") do\n task :classifier, classifier, depends_on: :none\n task :billing, billing_robot, depends_on: :optional\n task :technical, technical_robot, depends_on: :optional\n task :general, general_robot, depends_on: :optional\nend\n</code></pre>"},{"location":"guides/creating-networks/#running-networks","title":"Running Networks","text":""},{"location":"guides/creating-networks/#basic-run","title":"Basic Run","text":"<pre><code>result = network.run(message: \"Help me with my order\")\n\n# Get the final response\nputs result.value.last_text_content\n</code></pre>"},{"location":"guides/creating-networks/#with-additional-context","title":"With Additional Context","text":"<pre><code>result = network.run(\n message: \"Check my order status\",\n customer_id: 123,\n order_id: \"ORD-456\"\n)\n</code></pre>"},{"location":"guides/creating-networks/#accessing-task-results","title":"Accessing Task Results","text":"<pre><code>result = network.run(message: \"Process this\")\n\n# Access individual robot results\nclassifier_result = result.context[:classifier]\nbilling_result = result.context[:billing]\n\n# Original run parameters\noriginal_params = result.context[:run_params]\n</code></pre>"},{"location":"guides/creating-networks/#simpleflowresult","title":"SimpleFlow::Result","text":"<p>Networks return a <code>SimpleFlow::Result</code> object:</p> <pre><code>result = network.run(message: \"Hello\")\n\nresult.value # The final task's output (RobotResult)\nresult.context # Hash of all task results and metadata\nresult.halted? # Whether execution was halted early\nresult.continued? # Whether execution continued normally\n</code></pre>"},{"location":"guides/creating-networks/#broadcasting","title":"Broadcasting","text":"<p>Networks support a broadcast channel for network-wide announcements:</p> <pre><code># Register a broadcast handler\nnetwork.on_broadcast do |message|\n case message[:payload][:event]\n when :pause\n puts \"Pausing: #{message[:payload][:reason]}\"\n when :phase_complete\n puts \"Phase complete: #{message[:payload][:phase]}\"\n end\nend\n\n# Send broadcasts during execution\nnetwork.broadcast(event: :phase_complete, phase: \"analysis\")\n</code></pre>"},{"location":"guides/creating-networks/#patterns","title":"Patterns","text":""},{"location":"guides/creating-networks/#classifier-pattern","title":"Classifier Pattern","text":"<p>Route to specialists based on classification:</p> <pre><code>class SupportClassifier &lt; RobotLab::Robot\n def call(result)\n context = extract_run_context(result)\n message = context.delete(:message)\n robot_result = run(message, **context)\n\n new_result = result\n .with_context(@name.to_sym, robot_result)\n .continue(robot_result)\n\n category = robot_result.last_text_content.to_s.strip.downcase\n new_result.activate(category.to_sym)\n end\nend\n\nnetwork = RobotLab.create_network(name: \"support\") do\n task :classifier, SupportClassifier.new(name: \"classifier\", template: :classifier),\n depends_on: :none\n task :billing, billing_robot, depends_on: :optional\n task :technical, technical_robot, depends_on: :optional\n task :general, general_robot, depends_on: :optional\nend\n</code></pre>"},{"location":"guides/creating-networks/#pipeline-pattern","title":"Pipeline Pattern","text":"<p>Process through sequential stages:</p> <pre><code>network = RobotLab.create_network(name: \"document_processor\") do\n task :extract, extractor, depends_on: :none\n task :analyze, analyzer, depends_on: [:extract]\n task :format, formatter, depends_on: [:analyze]\nend\n</code></pre>"},{"location":"guides/creating-networks/#fan-outfan-in-pattern","title":"Fan-Out/Fan-In Pattern","text":"<p>Parallel processing with aggregation:</p> <pre><code>network = RobotLab.create_network(name: \"multi_analysis\") do\n task :prepare, preparer, depends_on: :none\n\n # Fan-out: parallel analysis\n task :sentiment, sentiment_analyzer, depends_on: [:prepare]\n task :topics, topic_extractor, depends_on: [:prepare]\n task :entities, entity_recognizer, depends_on: [:prepare]\n\n # Fan-in: aggregate results\n task :aggregate, aggregator, depends_on: [:sentiment, :topics, :entities]\nend\n</code></pre>"},{"location":"guides/creating-networks/#conditional-continuation","title":"Conditional Continuation","text":"<p>A robot can halt execution early:</p> <pre><code>class ValidatorRobot &lt; RobotLab::Robot\n def call(result)\n context = extract_run_context(result)\n message = context.delete(:message)\n robot_result = run(message, **context)\n\n if robot_result.last_text_content.include?(\"INVALID\")\n # Stop the pipeline\n result.halt(robot_result)\n else\n # Continue to next task\n result\n .with_context(@name.to_sym, robot_result)\n .continue(robot_result)\n end\n end\nend\n</code></pre>"},{"location":"guides/creating-networks/#data-passing-between-tasks","title":"Data Passing Between Tasks","text":"<p>Access previous task results via context:</p> <pre><code>class ResponderRobot &lt; RobotLab::Robot\n def call(result)\n # Get classifier's output\n classification = result.context[:classifier]&amp;.last_text_content\n\n context = extract_run_context(result)\n message = context.delete(:message)\n\n # Use classification in the message or context\n robot_result = run(\n \"Classification: #{classification}\\n\\nUser message: #{message}\",\n **context\n )\n\n result.with_context(@name.to_sym, robot_result).continue(robot_result)\n end\nend\n</code></pre>"},{"location":"guides/creating-networks/#visualization","title":"Visualization","text":""},{"location":"guides/creating-networks/#ascii-visualization","title":"ASCII Visualization","text":"<pre><code>puts network.visualize\n# =&gt; ASCII representation of the pipeline\n</code></pre>"},{"location":"guides/creating-networks/#mermaid-diagram","title":"Mermaid Diagram","text":"<pre><code>puts network.to_mermaid\n# =&gt; Mermaid graph definition\n</code></pre>"},{"location":"guides/creating-networks/#dot-format-graphviz","title":"DOT Format (Graphviz)","text":"<pre><code>puts network.to_dot\n# =&gt; Graphviz DOT format\n</code></pre>"},{"location":"guides/creating-networks/#execution-plan","title":"Execution Plan","text":"<pre><code>puts network.execution_plan\n# =&gt; Description of execution order\n</code></pre>"},{"location":"guides/creating-networks/#network-introspection","title":"Network Introspection","text":"<pre><code>network.name # =&gt; \"support\"\nnetwork.robots # =&gt; Hash of name =&gt; Robot\nnetwork.robot(:billing) # =&gt; Robot instance\nnetwork[\"billing\"] # =&gt; Robot instance (alias)\nnetwork.available_robots # =&gt; Array of Robot instances\nnetwork.memory # =&gt; Memory instance (shared)\nnetwork.to_h # =&gt; Hash representation\n</code></pre>"},{"location":"guides/creating-networks/#configuration-inheritance","title":"Configuration Inheritance","text":"<p>Networks accept a <code>config:</code> parameter that establishes default LLM settings for all member robots. This is useful when you want consistent behavior across a pipeline without configuring each robot individually.</p>"},{"location":"guides/creating-networks/#network-wide-defaults","title":"Network-Wide Defaults","text":"<pre><code># All robots in this network use the same model and temperature\nshared = RobotLab::RunConfig.new(model: \"claude-sonnet-4\", temperature: 0.5)\n\nnetwork = RobotLab.create_network(name: \"pipeline\", config: shared) do\n task :analyzer, analyzer_robot, depends_on: :none\n task :writer, writer_robot, depends_on: [:analyzer]\n task :reviewer, reviewer_robot, depends_on: [:writer]\nend\n</code></pre>"},{"location":"guides/creating-networks/#per-task-overrides","title":"Per-Task Overrides","text":"<p>Individual tasks can override the network's config with their own <code>config:</code>:</p> <pre><code>creative_config = RobotLab::RunConfig.new(temperature: 0.9)\n\nnetwork = RobotLab.create_network(name: \"pipeline\", config: shared) do\n task :analyzer, analyzer_robot, depends_on: :none\n task :writer, writer_robot,\n config: creative_config, # writer gets higher temperature\n depends_on: [:analyzer]\n task :reviewer, reviewer_robot, depends_on: [:writer]\nend\n</code></pre>"},{"location":"guides/creating-networks/#inheritance-chain","title":"Inheritance Chain","text":"<p>The full configuration hierarchy (most-specific wins):</p> <pre><code>RobotLab.config (global)\n -&gt; Network config\n -&gt; Task config\n -&gt; Robot config (from constructor)\n -&gt; Template front matter\n -&gt; Constructor kwargs (model:, temperature:, etc.)\n</code></pre> <p>Each layer only overrides values it explicitly sets. Unset values pass through from the parent.</p>"},{"location":"guides/creating-networks/#best-practices","title":"Best Practices","text":""},{"location":"guides/creating-networks/#1-keep-robots-focused","title":"1. Keep Robots Focused","text":"<p>Each robot should have a single responsibility:</p> <pre><code># Good: focused robots\ntask :classify, classifier, depends_on: :none\ntask :respond, responder, depends_on: [:classify]\n\n# Bad: one robot doing everything\ntask :do_everything, mega_robot, depends_on: :none\n</code></pre>"},{"location":"guides/creating-networks/#2-use-per-task-context","title":"2. Use Per-Task Context","text":"<p>Pass task-specific configuration through context:</p> <pre><code>task :billing, billing_robot,\n context: { department: \"billing\", max_refund: 500 },\n depends_on: :optional\n</code></pre>"},{"location":"guides/creating-networks/#3-handle-missing-results","title":"3. Handle Missing Results","text":"<p>Guard against missing optional task results:</p> <pre><code>def call(result)\n # Check if optional task ran\n if result.context[:validator]\n # Use validator result\n else\n # Handle missing validation\n end\nend\n</code></pre>"},{"location":"guides/creating-networks/#4-reset-memory-between-runs","title":"4. Reset Memory Between Runs","text":"<p>If reusing a network, reset shared memory between runs:</p> <pre><code>network.reset_memory\nresult = network.run(message: \"New request\")\n</code></pre>"},{"location":"guides/creating-networks/#next-steps","title":"Next Steps","text":"<ul> <li>Using Tools - Add capabilities to robots</li> <li>Memory Guide - Shared memory patterns</li> <li>API Reference: Network - Complete API</li> </ul>"},{"location":"guides/mcp-integration/","title":"MCP Integration","text":"<p>RobotLab supports the Model Context Protocol (MCP) for connecting to external tool servers.</p>"},{"location":"guides/mcp-integration/#what-is-mcp","title":"What is MCP?","text":"<p>MCP is a protocol that allows LLM applications to connect to external servers that provide tools, resources, and context. This enables:</p> <ul> <li>Reusable tool servers across applications</li> <li>Separation of tool logic from AI logic</li> <li>Dynamic tool discovery</li> </ul>"},{"location":"guides/mcp-integration/#configuring-mcp-servers","title":"Configuring MCP Servers","text":""},{"location":"guides/mcp-integration/#at-robot-level","title":"At Robot Level","text":"<p>Use the <code>mcp:</code> parameter on <code>RobotLab.build</code> to connect a robot to MCP servers:</p> <pre><code>robot = RobotLab.build(\n name: \"coder\",\n template: :developer,\n mcp: [\n {\n name: \"filesystem\",\n transport: {\n type: \"stdio\",\n command: \"mcp-server-filesystem\",\n args: [\"--root\", \"/home/user/projects\"]\n }\n },\n {\n name: \"github\",\n transport: {\n type: \"stdio\",\n command: \"mcp-server-github\",\n env: { \"GITHUB_TOKEN\" =&gt; ENV[\"GITHUB_TOKEN\"] }\n }\n }\n ]\n)\n</code></pre>"},{"location":"guides/mcp-integration/#in-template-front-matter","title":"In Template Front Matter","text":"<p>MCP servers can be declared directly in a template's YAML front matter, making the template fully self-contained:</p> prompts/github_assistant.md<pre><code>---\ndescription: GitHub assistant with MCP tool access\nmcp:\n - name: github\n transport: stdio\n command: npx\n args: [\"-y\", \"@modelcontextprotocol/server-github\"]\n---\nYou are a helpful GitHub assistant with access to GitHub tools via MCP.\n</code></pre> <pre><code># MCP config comes from the template \u2014 no mcp: parameter needed\nrobot = RobotLab.build(template: :github_assistant)\n</code></pre> <p>Constructor <code>mcp:</code> overrides frontmatter <code>mcp:</code> when provided.</p>"},{"location":"guides/mcp-integration/#hierarchical-configuration","title":"Hierarchical Configuration","text":"<p>The <code>mcp:</code> parameter supports three modes:</p> Value Behavior <code>:none</code> No MCP servers (default) <code>:inherit</code> Inherit from network or global config <code>[...]</code> Explicit array of server configurations <pre><code># Inherit from network/config\nrobot = RobotLab.build(\n name: \"reader\",\n system_prompt: \"You help read files.\",\n mcp: :inherit\n)\n\n# Disable MCP explicitly\nrobot = RobotLab.build(\n name: \"calculator\",\n system_prompt: \"You do math.\",\n mcp: :none\n)\n</code></pre>"},{"location":"guides/mcp-integration/#resolution-order","title":"Resolution Order","text":"<p>MCP configuration resolves through a hierarchy: runtime &gt; robot build &gt; network &gt; global config. Each level can override the previous:</p> <pre><code>Global (RobotLab.config.mcp)\n -&gt; Network (task mcp: [...])\n -&gt; Robot (mcp: :inherit | :none | [...])\n -&gt; Runtime (robot.run(\"msg\", mcp: [...]))\n</code></pre>"},{"location":"guides/mcp-integration/#transport-types","title":"Transport Types","text":""},{"location":"guides/mcp-integration/#stdio-transport","title":"Stdio Transport","text":"<p>Communicate via stdin/stdout with a subprocess:</p> <pre><code>{\n name: \"server_name\",\n transport: {\n type: \"stdio\",\n command: \"mcp-server-command\",\n args: [\"--option\", \"value\"],\n env: { \"API_KEY\" =&gt; ENV[\"API_KEY\"] }\n }\n}\n</code></pre>"},{"location":"guides/mcp-integration/#websocket-transport","title":"WebSocket Transport","text":"<p>Connect via WebSocket:</p> <pre><code>{\n name: \"remote_server\",\n transport: {\n type: \"websocket\",\n url: \"ws://localhost:8080/mcp\"\n }\n}\n</code></pre> <p>Dependency Required</p> <p>WebSocket transport requires the <code>async-websocket</code> gem.</p>"},{"location":"guides/mcp-integration/#sse-transport","title":"SSE Transport","text":"<p>Server-Sent Events transport:</p> <pre><code>{\n name: \"sse_server\",\n transport: {\n type: \"sse\",\n url: \"http://localhost:8080/sse\"\n }\n}\n</code></pre>"},{"location":"guides/mcp-integration/#http-transport","title":"HTTP Transport","text":"<p>Streamable HTTP transport with session support:</p> <pre><code>{\n name: \"http_server\",\n transport: {\n type: \"streamable_http\",\n url: \"https://api.example.com/mcp\",\n session_id: \"optional_session_id\",\n auth_provider: -&gt; { \"Bearer #{fetch_token}\" }\n }\n}\n</code></pre>"},{"location":"guides/mcp-integration/#using-mcp-tools","title":"Using MCP Tools","text":"<p>Once configured, MCP tools are automatically discovered and made available to the robot. The robot connects to MCP servers on its first <code>run</code> call and discovers tools dynamically:</p> <pre><code>robot = RobotLab.build(\n name: \"helper\",\n system_prompt: &lt;&lt;~PROMPT\n You can help users with GitHub tasks.\n Use available tools to search repositories, create issues, etc.\n PROMPT,\n mcp: [\n { name: \"github\", transport: { type: \"stdio\", command: \"mcp-server-github\" } }\n ]\n)\n\n# MCP tools are automatically available\nresult = robot.run(\"Find repositories about machine learning\")\nputs result.last_text_content\n You can help users with GitHub tasks.\n Use available tools to search repositories, create issues, etc.\n PROMPT,\n mcp: [\n { name: \"github\", transport: { type: \"stdio\", command: \"mcp-server-github\" } }\n ]\n)\n\n# MCP tools are automatically available\nresult = robot.run(\"Find repositories about machine learning\")\nputs result.last_text_content\n</code></pre>"},{"location":"guides/mcp-integration/#filtering-mcp-tools","title":"Filtering MCP Tools","text":"<p>Use the <code>tools:</code> parameter to restrict which tools (including MCP-discovered tools) are available to a robot:</p> <pre><code>robot = RobotLab.build(\n name: \"reader\",\n system_prompt: \"You help read and search files.\",\n mcp: [\n { name: \"filesystem\", transport: { type: \"stdio\", command: \"mcp-server-fs\" } }\n ],\n tools: %w[read_file search_files list_directory] # Only allow specific tools\n)\n</code></pre>"},{"location":"guides/mcp-integration/#mcp-in-networks","title":"MCP in Networks","text":"<p>When running robots in a network, use per-task MCP configuration:</p> <pre><code>network = RobotLab.create_network(name: \"dev_pipeline\") do\n task :planner, planner_robot, depends_on: :none\n task :coder, coder_robot,\n mcp: [\n { name: \"filesystem\", transport: { type: \"stdio\", command: \"mcp-server-fs\" } }\n ],\n depends_on: [:planner]\n task :reviewer, reviewer_robot, depends_on: [:coder]\nend\n</code></pre>"},{"location":"guides/mcp-integration/#common-mcp-servers","title":"Common MCP Servers","text":""},{"location":"guides/mcp-integration/#filesystem","title":"Filesystem","text":"<pre><code>{\n name: \"filesystem\",\n transport: {\n type: \"stdio\",\n command: \"mcp-server-filesystem\",\n args: [\"--root\", \"/path/to/files\"]\n }\n}\n</code></pre> <p>Tools: <code>read_file</code>, <code>write_file</code>, <code>list_directory</code>, <code>search_files</code></p>"},{"location":"guides/mcp-integration/#github","title":"GitHub","text":"<pre><code>{\n name: \"github\",\n transport: {\n type: \"stdio\",\n command: \"mcp-server-github\",\n env: { \"GITHUB_TOKEN\" =&gt; ENV[\"GITHUB_TOKEN\"] }\n }\n}\n</code></pre> <p>Tools: <code>search_repositories</code>, <code>create_issue</code>, <code>get_file_contents</code>, etc.</p>"},{"location":"guides/mcp-integration/#database","title":"Database","text":"<pre><code>{\n name: \"postgres\",\n transport: {\n type: \"stdio\",\n command: \"mcp-server-postgres\",\n env: { \"DATABASE_URL\" =&gt; ENV[\"DATABASE_URL\"] }\n }\n}\n</code></pre> <p>Tools: <code>query</code>, <code>list_tables</code>, <code>describe_table</code></p>"},{"location":"guides/mcp-integration/#mcp-server-and-client-objects","title":"MCP Server and Client Objects","text":"<p>For programmatic access, you can work with MCP objects directly:</p> <pre><code># Server configuration\nserver = RobotLab::MCP::Server.new(\n name: \"my_server\",\n transport: {\n type: \"stdio\",\n command: \"my-mcp-server\"\n }\n)\n\n# Client connection\nclient = RobotLab::MCP::Client.new(server)\nclient.connect\n\nclient.connected? # =&gt; true\nclient.list_tools # =&gt; Array of tool definitions\nclient.call_tool(\"search\", { query: \"ruby\" })\nclient.list_resources # =&gt; Array of resource definitions\nclient.disconnect\n</code></pre>"},{"location":"guides/mcp-integration/#error-handling","title":"Error Handling","text":""},{"location":"guides/mcp-integration/#connection-errors","title":"Connection Errors","text":"<pre><code>begin\n result = robot.run(\"Search for repos\")\nrescue RobotLab::MCPError =&gt; e\n puts \"MCP Error: #{e.message}\"\nend\n</code></pre> <p>Tip</p> <p>MCP connection failures are logged as warnings but do not raise errors by default. The robot will continue without MCP tools if a server is unreachable.</p>"},{"location":"guides/mcp-integration/#disconnecting","title":"Disconnecting","text":"<p>Robots can be manually disconnected from MCP servers:</p> <pre><code>robot.disconnect # Disconnect all MCP clients\n</code></pre>"},{"location":"guides/mcp-integration/#patterns","title":"Patterns","text":""},{"location":"guides/mcp-integration/#development-vs-production","title":"Development vs Production","text":"<pre><code>mcp_config = if Rails.env.development?\n [{ name: \"local_fs\", transport: { type: \"stdio\", command: \"mcp-fs\", args: [\"--root\", \".\"] } }]\nelse\n [{ name: \"s3\", transport: { type: \"stdio\", command: \"mcp-s3\" } }]\nend\n\nrobot = RobotLab.build(\n name: \"file_handler\",\n system_prompt: \"You manage files.\",\n mcp: mcp_config\n)\n</code></pre>"},{"location":"guides/mcp-integration/#dynamic-server-selection","title":"Dynamic Server Selection","text":"<pre><code>def mcp_servers_for_user(user)\n servers = []\n servers &lt;&lt; github_server if user.github_connected?\n servers &lt;&lt; slack_server if user.slack_connected?\n servers\nend\n\nrobot = RobotLab.build(\n name: \"assistant\",\n system_prompt: \"You help the user with connected services.\",\n mcp: mcp_servers_for_user(current_user)\n)\n</code></pre>"},{"location":"guides/mcp-integration/#best-practices","title":"Best Practices","text":""},{"location":"guides/mcp-integration/#1-use-environment-variables-for-credentials","title":"1. Use Environment Variables for Credentials","text":"<pre><code>{\n name: \"github\",\n transport: {\n type: \"stdio\",\n command: \"mcp-server-github\",\n env: {\n \"GITHUB_TOKEN\" =&gt; ENV[\"GITHUB_TOKEN\"],\n \"GITHUB_ORG\" =&gt; ENV[\"GITHUB_ORG\"]\n }\n }\n}\n</code></pre>"},{"location":"guides/mcp-integration/#2-limit-tool-access","title":"2. Limit Tool Access","text":"<p>Restrict which MCP tools are available to a robot using the <code>tools:</code> parameter:</p> <pre><code>robot = RobotLab.build(\n name: \"reader\",\n system_prompt: \"You read and search files.\",\n mcp: [{ name: \"fs\", transport: { type: \"stdio\", command: \"mcp-fs\" } }],\n tools: %w[read_file search_files] # No write access\n)\n</code></pre>"},{"location":"guides/mcp-integration/#3-use-appropriate-transports","title":"3. Use Appropriate Transports","text":"Transport Best For <code>stdio</code> Local servers, CLI tools <code>websocket</code> Persistent connections, bidirectional <code>sse</code> Server push, event streams <code>streamable_http</code> Remote APIs, session-based"},{"location":"guides/mcp-integration/#next-steps","title":"Next Steps","text":"<ul> <li>Using Tools - Local tool patterns</li> <li>Creating Networks - Network configuration</li> <li>API Reference: MCP - Complete MCP API</li> </ul>"},{"location":"guides/memory/","title":"Memory System","text":"<p>The memory system provides key-value storage for robots, supporting both standalone and network execution modes.</p>"},{"location":"guides/memory/#overview","title":"Overview","text":"<p>Memory is a reactive key-value store that provides:</p> <ul> <li>Key-value storage with <code>[]</code> and <code>[]=</code> accessors</li> <li>Reserved keys for structured data (<code>:data</code>, <code>:results</code>, <code>:messages</code>, <code>:session_id</code>, <code>:cache</code>)</li> <li>Reactive subscriptions and blocking reads for inter-robot communication</li> <li>Optional Redis backend for persistence</li> <li>Semantic caching via <code>RubyLLM::SemanticCache</code></li> </ul>"},{"location":"guides/memory/#standalone-robot-memory","title":"Standalone Robot Memory","text":"<p>Every robot has its own inherent memory that persists across runs:</p> <pre><code>robot = RobotLab.build(\n name: \"assistant\",\n system_prompt: \"You are helpful.\"\n)\n\n# Memory persists across runs\nrobot.memory[:user_name] = \"Alice\"\nrobot.memory[:preferences] = { theme: \"dark\", language: \"en\" }\n\nresult = robot.run(\"Hello!\")\n\n# Read it back later\nrobot.memory[:user_name] # =&gt; \"Alice\"\nrobot.memory[:preferences] # =&gt; { theme: \"dark\", language: \"en\" }\n</code></pre>"},{"location":"guides/memory/#basic-operations","title":"Basic Operations","text":""},{"location":"guides/memory/#store-values","title":"Store Values","text":"<pre><code>robot.memory[:key] = \"value\"\nrobot.memory[:count] = 42\nrobot.memory[:config] = { timeout: 30, retries: 3 }\n</code></pre>"},{"location":"guides/memory/#retrieve-values","title":"Retrieve Values","text":"<pre><code>name = robot.memory[:user_name] # =&gt; \"Alice\"\nmissing = robot.memory[:unknown] # =&gt; nil\n</code></pre>"},{"location":"guides/memory/#check-existence","title":"Check Existence","text":"<pre><code>robot.memory.key?(:user_name) # =&gt; true\nrobot.memory.key?(:unknown) # =&gt; false\n</code></pre>"},{"location":"guides/memory/#delete-values","title":"Delete Values","text":"<pre><code>robot.memory.delete(:temp_data)\n</code></pre>"},{"location":"guides/memory/#list-keys","title":"List Keys","text":"<pre><code>robot.memory.keys # =&gt; [:user_name, :preferences] (excludes reserved keys)\nrobot.memory.all_keys # =&gt; [:data, :results, :messages, :session_id, :cache, :user_name, ...]\n</code></pre>"},{"location":"guides/memory/#merge-values","title":"Merge Values","text":"<pre><code>robot.memory.merge!(user_id: 123, session: \"abc\")\n</code></pre>"},{"location":"guides/memory/#reserved-keys","title":"Reserved Keys","text":"<p>Memory has reserved keys with special behavior:</p> Key Type Description <code>:data</code> Hash (StateProxy) Runtime data with method-style access <code>:results</code> Array Accumulated robot results <code>:messages</code> Array Conversation history <code>:session_id</code> String Session identifier for history persistence <code>:cache</code> SemanticCache Semantic cache (read-only after init)"},{"location":"guides/memory/#the-data-hash","title":"The Data Hash","text":"<p>The <code>:data</code> key provides a <code>StateProxy</code> for method-style access:</p> <pre><code>robot.memory.data[:category] = \"billing\"\nrobot.memory.data.category # =&gt; \"billing\" (method-style access)\nrobot.memory.data.to_h # =&gt; { category: \"billing\" }\n</code></pre>"},{"location":"guides/memory/#results-and-messages","title":"Results and Messages","text":"<pre><code>robot.memory.results # =&gt; Array of RobotResult objects\nrobot.memory.messages # =&gt; Array of Message objects\nrobot.memory.session_id # =&gt; \"abc123\" or nil\n</code></pre>"},{"location":"guides/memory/#runtime-memory-injection","title":"Runtime Memory Injection","text":"<p>Pass memory values for a single run using the <code>memory:</code> keyword:</p> <pre><code># Inject a hash -- values are merged into the active memory\nresult = robot.run(\"What's my order status?\", memory: { user_id: 123, order_id: \"ORD-456\" })\n\n# The robot's memory now contains those keys\nrobot.memory[:user_id] # =&gt; 123\nrobot.memory[:order_id] # =&gt; \"ORD-456\"\n</code></pre> <p>You can also pass a full <code>Memory</code> object to replace the active memory for that run:</p> <pre><code>custom_memory = RobotLab.create_memory(data: { user_id: 123 })\ncustom_memory[:context] = \"billing inquiry\"\n\nresult = robot.run(\"Help me\", memory: custom_memory)\n</code></pre>"},{"location":"guides/memory/#resetting-memory","title":"Resetting Memory","text":"<p>Clear a robot's memory back to its initial state:</p> <pre><code>robot.reset_memory\nrobot.memory.keys # =&gt; [] (custom keys cleared, reserved keys reset)\n</code></pre> <p>You can also clear just the custom keys without resetting reserved keys:</p> <pre><code>robot.memory.clear # Clears non-reserved keys only\n</code></pre>"},{"location":"guides/memory/#network-shared-memory","title":"Network Shared Memory","text":"<p>When robots run in a network, they share the network's memory instead of using their own inherent memory. This allows robots to communicate through shared state:</p> <pre><code>network = RobotLab.create_network(name: \"pipeline\") do\n task :analyzer, analyzer_robot, depends_on: :none\n task :writer, writer_robot, depends_on: [:analyzer]\nend\n\n# All robots in the network share this memory\nnetwork.memory[:project] = \"quarterly_report\"\n\nresult = network.run(message: \"Analyze sales data\")\n\n# After the run, shared memory contains values written by all robots\nnetwork.memory[:analysis_result] # Written by analyzer\nnetwork.memory[:draft] # Written by writer\n</code></pre>"},{"location":"guides/memory/#resetting-network-memory","title":"Resetting Network Memory","text":"<pre><code>network.reset_memory # Clear shared memory between runs\n</code></pre>"},{"location":"guides/memory/#reactive-memory","title":"Reactive Memory","text":"<p>Memory supports reactive features for concurrent robot execution.</p>"},{"location":"guides/memory/#blocking-reads","title":"Blocking Reads","text":"<p>Wait for a value to become available (useful in parallel pipelines):</p> <pre><code># In robot A (writer)\nmemory.set(:sentiment, { score: 0.8, confidence: 0.95 })\n\n# In robot B (reader, may run concurrently)\nresult = memory.get(:sentiment, wait: true) # Blocks until available\nresult = memory.get(:sentiment, wait: 30) # Blocks up to 30 seconds\n\n# Multiple keys\nresults = memory.get(:sentiment, :entities, :keywords, wait: 60)\n# =&gt; { sentiment: {...}, entities: [...], keywords: [...] }\n</code></pre>"},{"location":"guides/memory/#subscriptions","title":"Subscriptions","text":"<p>Subscribe to key changes with asynchronous callbacks:</p> <pre><code># Subscribe to a single key\nmemory.subscribe(:raw_data) do |change|\n puts \"#{change.key} changed by #{change.writer}\"\n puts \"Old: #{change.previous}, New: #{change.value}\"\nend\n\n# Subscribe to multiple keys\nmemory.subscribe(:sentiment, :entities) do |change|\n update_dashboard(change.key, change.value)\nend\n\n# Pattern subscriptions (glob-style)\nmemory.subscribe_pattern(\"analysis:*\") do |change|\n puts \"Analysis key #{change.key} updated\"\nend\n</code></pre>"},{"location":"guides/memory/#unsubscribe","title":"Unsubscribe","text":"<pre><code>sub_id = memory.subscribe(:status) { |c| puts c.value }\nmemory.unsubscribe(sub_id)\n</code></pre>"},{"location":"guides/memory/#creating-standalone-memory","title":"Creating Standalone Memory","text":"<p>Use the factory method for standalone memory objects:</p> <pre><code>memory = RobotLab.create_memory(\n data: { user_id: 123, category: nil },\n enable_cache: true\n)\n\nmemory[:session_id] = \"abc123\"\nmemory[:custom_key] = \"custom_value\"\n</code></pre>"},{"location":"guides/memory/#serialization","title":"Serialization","text":"<p>Memory can be exported and reconstructed:</p> <pre><code># Export to hash\nhash = robot.memory.to_h\n# =&gt; { data: {...}, results: [...], messages: [...], session_id: \"...\", custom: {...} }\n\n# Export to JSON\njson = robot.memory.to_json\n\n# Reconstruct from hash\nrestored = RobotLab::Memory.from_hash(hash)\n</code></pre>"},{"location":"guides/memory/#patterns","title":"Patterns","text":""},{"location":"guides/memory/#accumulating-data-across-robots","title":"Accumulating Data Across Robots","text":"<pre><code># In each robot's processing\ndef accumulate_finding(memory, finding)\n findings = memory[:findings] || []\n findings &lt;&lt; finding\n memory[:findings] = findings\nend\n\n# In the final robot\nall_findings = memory[:findings]\n</code></pre>"},{"location":"guides/memory/#tracking-progress","title":"Tracking Progress","text":"<pre><code>memory[:stage] = \"intake\"\n# ... processing ...\nmemory[:stage] = \"analysis\"\n# ... processing ...\nmemory[:stage] = \"response\"\n</code></pre>"},{"location":"guides/memory/#caching-expensive-operations","title":"Caching Expensive Operations","text":"<pre><code>class FetchUser &lt; RubyLLM::Tool\n description \"Fetch user details by ID\"\n param :user_id, type: :string, desc: \"User ID\"\n\n def execute(user_id:)\n cache_key = \"cache:user:#{user_id}\"\n\n # Check robot's memory for cached value\n # (In practice, you'd access memory through the robot's context)\n cached = Thread.current[:robot_memory]&amp;.[](cache_key.to_sym)\n return cached if cached\n\n # Fetch and cache\n user = User.find(user_id).to_h\n Thread.current[:robot_memory]&amp;.[]=(cache_key.to_sym, user)\n user\n end\nend\n</code></pre>"},{"location":"guides/memory/#semantic-caching","title":"Semantic Caching","text":"<p>Memory includes a semantic cache for LLM response caching:</p> <pre><code># Access the semantic cache\ncache = robot.memory.cache # =&gt; RubyLLM::SemanticCache\n\n# Use it to cache semantically similar queries\nresponse = cache.fetch(\"What is Ruby?\") do\n robot.run(\"What is Ruby?\")\nend\n</code></pre>"},{"location":"guides/memory/#best-practices","title":"Best Practices","text":""},{"location":"guides/memory/#1-use-descriptive-keys","title":"1. Use Descriptive Keys","text":"<pre><code># Good\nrobot.memory[:classification_intent] = \"billing\"\nrobot.memory[:user_last_order_id] = \"ord_456\"\n\n# Bad\nrobot.memory[:x] = \"billing\"\nrobot.memory[:temp1] = \"ord_456\"\n</code></pre>"},{"location":"guides/memory/#2-use-data-hash-for-structured-runtime-input","title":"2. Use Data Hash for Structured Runtime Input","text":"<pre><code>memory = RobotLab.create_memory(\n data: { order_id: \"123\", priority: \"high\", customer_tier: \"gold\" }\n)\n\n# Access via data proxy\nmemory.data.order_id # =&gt; \"123\"\nmemory.data.priority # =&gt; \"high\"\nmemory.data.customer_tier # =&gt; \"gold\"\n</code></pre>"},{"location":"guides/memory/#3-clean-up-temporary-values","title":"3. Clean Up Temporary Values","text":"<pre><code># After processing is done\nrobot.memory.delete(:temp_calculation)\nrobot.memory.delete(:intermediate_result)\n</code></pre>"},{"location":"guides/memory/#4-document-memory-keys","title":"4. Document Memory Keys","text":"<pre><code># In your robot definitions, document expected keys:\n#\n# Memory keys used by this pipeline:\n# - :intent - Classification result (set by classifier)\n# - :entities - Extracted entities (set by entity_extractor)\n# - :response - Final response draft (set by responder)\n</code></pre>"},{"location":"guides/memory/#next-steps","title":"Next Steps","text":"<ul> <li>Building Robots - Using memory in robots</li> <li>Creating Networks - Shared memory in networks</li> <li>API Reference: Memory - Complete API</li> </ul>"},{"location":"guides/rails-integration/","title":"Rails Integration","text":"<p>RobotLab integrates seamlessly with Ruby on Rails applications.</p>"},{"location":"guides/rails-integration/#installation","title":"Installation","text":""},{"location":"guides/rails-integration/#generate-files","title":"Generate Files","text":"<pre><code>rails generate robot_lab:install\n</code></pre> <p>This creates:</p> <pre><code>config/initializers/robot_lab.rb # Logger setup\ndb/migrate/*_create_robot_lab_tables.rb # Database tables\napp/models/robot_lab_thread.rb # Thread model\napp/models/robot_lab_result.rb # Result model\napp/jobs/robot_run_job.rb # Background job for robot execution\napp/robots/ # Directory for robots\napp/tools/ # Directory for tools\n</code></pre> <p>Options:</p> <ul> <li><code>--skip-migration</code> \u2014 Skip database migration generation</li> <li><code>--skip-job</code> \u2014 Skip background job generation</li> </ul>"},{"location":"guides/rails-integration/#run-migrations","title":"Run Migrations","text":"<pre><code>rails db:migrate\n</code></pre>"},{"location":"guides/rails-integration/#configuration","title":"Configuration","text":"<p>RobotLab uses MywayConfig for configuration. There is no <code>RobotLab.configure</code> block. Instead, settings are loaded from YAML files and environment variables in the following priority order:</p> <ol> <li>Bundled defaults (<code>lib/robot_lab/config/defaults.yml</code>)</li> <li>Environment-specific overrides (development, test, production sections)</li> <li>XDG user config (<code>~/.config/robot_lab/config.yml</code>)</li> <li>Project config (<code>./config/robot_lab.yml</code>)</li> <li>Environment variables (<code>ROBOT_LAB_*</code> prefix)</li> </ol>"},{"location":"guides/rails-integration/#project-config-file","title":"Project Config File","text":"config/robot_lab.yml<pre><code>defaults:\n ruby_llm:\n anthropic_api_key: &lt;%= ENV['ANTHROPIC_API_KEY'] %&gt;\n openai_api_key: &lt;%= ENV['OPENAI_API_KEY'] %&gt;\n model: claude-sonnet-4\n request_timeout: 180\n\n # Template path auto-detected as app/prompts in Rails\n # template_path: app/prompts\n\ndevelopment:\n ruby_llm:\n model: claude-haiku-3\n log_level: :debug\n\ntest:\n streaming_enabled: false\n ruby_llm:\n model: claude-3-haiku-20240307\n request_timeout: 30\n\nproduction:\n ruby_llm:\n request_timeout: 180\n max_retries: 5\n</code></pre>"},{"location":"guides/rails-integration/#environment-variables","title":"Environment Variables","text":"<p>Environment variables use the <code>ROBOT_LAB_</code> prefix with double underscores for nested keys:</p> <pre><code>ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...\nROBOT_LAB_RUBY_LLM__MODEL=claude-sonnet-4\nROBOT_LAB_RUBY_LLM__REQUEST_TIMEOUT=180\n</code></pre> <p>RobotLab also falls back to standard provider environment variables (e.g. <code>ANTHROPIC_API_KEY</code>, <code>OPENAI_API_KEY</code>) when the prefixed versions are not set.</p>"},{"location":"guides/rails-integration/#initializer-logger-only","title":"Initializer (Logger Only)","text":"<p>The only runtime-writable config attribute is the logger. The generated initializer sets it to the Rails logger:</p> config/initializers/robot_lab.rb<pre><code># frozen_string_literal: true\n\n# Set the RobotLab logger to use Rails.logger\nRobotLab.config.logger = Rails.logger\n</code></pre>"},{"location":"guides/rails-integration/#accessing-configuration","title":"Accessing Configuration","text":"<pre><code># Read configuration values\nRobotLab.config.ruby_llm.model #=&gt; \"claude-sonnet-4\"\nRobotLab.config.ruby_llm.anthropic_api_key #=&gt; \"sk-ant-...\"\nRobotLab.config.ruby_llm.request_timeout #=&gt; 120\nRobotLab.config.streaming_enabled #=&gt; true\n</code></pre>"},{"location":"guides/rails-integration/#creating-robots","title":"Creating Robots","text":""},{"location":"guides/rails-integration/#robot-generator","title":"Robot Generator","text":"<pre><code>rails generate robot_lab:robot Support\nrails generate robot_lab:robot Billing --description=\"Handles billing inquiries\"\nrails generate robot_lab:robot Router --routing\n</code></pre>"},{"location":"guides/rails-integration/#robot-class","title":"Robot Class","text":"<p>Robots are plain Ruby classes with a <code>.build</code> factory method that calls <code>RobotLab.build</code> with keyword arguments:</p> app/robots/support_robot.rb<pre><code># frozen_string_literal: true\n\nclass SupportRobot\n def self.build(**options)\n RobotLab.build(\n name: \"support\",\n description: \"Handles customer support inquiries\",\n system_prompt: \"You are a helpful support assistant.\",\n model: \"claude-sonnet-4\",\n local_tools: [OrderLookup],\n **options\n )\n end\nend\n</code></pre>"},{"location":"guides/rails-integration/#routing-robot-class","title":"Routing Robot Class","text":"<p>A routing robot classifies requests and activates optional tasks in a Network. It subclasses <code>RobotLab::Robot</code> and overrides <code>call(result)</code>:</p> app/robots/classifier_robot.rb<pre><code># frozen_string_literal: true\n\nclass ClassifierRobot &lt; RobotLab::Robot\n SYSTEM_PROMPT = &lt;&lt;~PROMPT\n You are a routing robot that classifies user requests.\n\n Analyze the user's request and respond with ONLY the category name.\n Valid categories: billing, technical, general\n PROMPT\n\n def self.build(**options)\n new(\n name: \"classifier\",\n description: \"Classifies support requests\",\n system_prompt: SYSTEM_PROMPT,\n **options\n )\n end\n\n def call(result)\n context = extract_run_context(result)\n message = context.delete(:message)\n\n robot_result = run(message, **context)\n\n new_result = result\n .with_context(@name.to_sym, robot_result)\n .continue(robot_result)\n\n category = robot_result.last_text_content.to_s.strip.downcase\n\n case category\n when /billing/ then new_result.activate(:billing)\n when /technical/ then new_result.activate(:technical)\n else new_result.activate(:general)\n end\n end\nend\n</code></pre> <p>Use the routing robot as the first task in a network:</p> <pre><code>classifier = ClassifierRobot.build\nbilling = BillingRobot.build\ntechnical = TechnicalRobot.build\n\nnetwork = RobotLab.create_network(name: \"support\") do\n task :classifier, classifier, depends_on: :none\n task :billing, billing, depends_on: :optional\n task :technical, technical, depends_on: :optional\nend\n\nresult = network.run(message: \"I was charged twice\")\n</code></pre>"},{"location":"guides/rails-integration/#custom-tool","title":"Custom Tool","text":"<p>Tools subclass <code>RobotLab::Tool</code> (which extends <code>RubyLLM::Tool</code>):</p> app/tools/order_lookup.rb<pre><code># frozen_string_literal: true\n\nclass OrderLookup &lt; RobotLab::Tool\n description \"Look up an order by ID\"\n param :order_id, type: \"string\", desc: \"The order ID to look up\"\n\n def execute(order_id:)\n order = Order.find_by(id: order_id)\n return \"Order not found\" unless order\n\n {\n id: order.id,\n status: order.status,\n total: order.total.to_s,\n created_at: order.created_at.iso8601\n }.to_json\n end\nend\n</code></pre>"},{"location":"guides/rails-integration/#using-in-controllers","title":"Using in Controllers","text":"app/controllers/chat_controller.rb<pre><code>class ChatController &lt; ApplicationController\n def create\n robot = SupportRobot.build\n result = robot.run(params[:message])\n\n render json: {\n response: result.last_text_content,\n robot_name: result.robot_name\n }\n end\nend\n</code></pre>"},{"location":"guides/rails-integration/#using-a-network-in-controllers","title":"Using a Network in Controllers","text":"<p>Networks use <code>RobotLab.create_network</code> with a block DSL that defines tasks. Each task wraps a robot with dependency declarations:</p> app/controllers/chat_controller.rb<pre><code>class ChatController &lt; ApplicationController\n def create\n support_robot = SupportRobot.build\n billing_robot = BillingRobot.build\n\n network = RobotLab.create_network(name: \"customer_service\") do\n task :support, support_robot, depends_on: :none\n task :billing, billing_robot, depends_on: :optional\n end\n\n result = network.run(message: params[:message], user_id: current_user.id)\n\n # result is a SimpleFlow::Result\n # result.value is a RobotResult from the last robot\n render json: {\n response: result.value.last_text_content,\n robot_name: result.value.robot_name\n }\n end\nend\n</code></pre>"},{"location":"guides/rails-integration/#prompt-templates","title":"Prompt Templates","text":""},{"location":"guides/rails-integration/#template-location","title":"Template Location","text":"<p>Templates are <code>.md</code> files with YAML front matter, stored in <code>app/prompts/</code> (auto-configured for Rails):</p> <pre><code>app/prompts/\n\u251c\u2500\u2500 support.md\n\u251c\u2500\u2500 billing.md\n\u2514\u2500\u2500 router.md\n</code></pre>"},{"location":"guides/rails-integration/#template-format","title":"Template Format","text":"app/prompts/support.md<pre><code>---\ndescription: Customer support assistant\nparameters:\n company_name: null\n tone: friendly\nmodel: claude-sonnet-4\ntemperature: 0.7\n---\nYou are a support agent for &lt;%= company_name %&gt;.\nRespond in a &lt;%= tone %&gt; manner.\n\nYour responsibilities:\n- Answer product questions\n- Help with order issues\n- Provide friendly assistance\n</code></pre>"},{"location":"guides/rails-integration/#template-usage","title":"Template Usage","text":"<pre><code># Pass context to fill template parameters\nrobot = RobotLab.build(\n name: \"support\",\n template: :support,\n context: { company_name: \"Acme Corp\" }\n)\n\n# Parameters with defaults (like `tone: friendly`) are optional.\n# Parameters set to null are required and must be provided via context.\nresult = robot.run(\"I need help with my order\")\n</code></pre>"},{"location":"guides/rails-integration/#action-cable-integration","title":"Action Cable Integration","text":""},{"location":"guides/rails-integration/#channel","title":"Channel","text":"app/channels/chat_channel.rb<pre><code>class ChatChannel &lt; ApplicationCable::Channel\n def subscribed\n stream_from \"chat_#{params[:session_id]}\"\n end\n\n def receive(data)\n message = data[\"message\"]\n session_id = data[\"session_id\"]\n\n robot = SupportRobot.build\n result = robot.run(message)\n\n ActionCable.server.broadcast(\n \"chat_#{session_id}\",\n {\n event: \"complete\",\n response: result.last_text_content,\n robot_name: result.robot_name\n }\n )\n end\nend\n</code></pre>"},{"location":"guides/rails-integration/#javascript-client","title":"JavaScript Client","text":"<pre><code>const channel = consumer.subscriptions.create(\n { channel: \"ChatChannel\", session_id: sessionId },\n {\n received(data) {\n if (data.event === \"complete\") {\n displayMessage(data.response);\n }\n }\n }\n);\n\nchannel.send({ message: \"Hello!\", session_id: sessionId });\n</code></pre>"},{"location":"guides/rails-integration/#background-jobs","title":"Background Jobs","text":""},{"location":"guides/rails-integration/#robotrunjob-generated","title":"RobotRunJob (Generated)","text":"<p>The install generator creates <code>app/jobs/robot_run_job.rb</code> \u2014 an ActiveJob class that wraps robot execution with result persistence and optional Turbo Stream broadcasting.</p> <pre><code># Enqueue from a controller\nRobotRunJob.perform_later(\n robot_class: \"SupportRobot\",\n message: params[:message],\n thread_id: session_id\n)\n\nrender json: { status: \"processing\" }\n</code></pre> <p>The job:</p> <ol> <li>Finds or creates a <code>RobotLabThread</code> by <code>thread_id</code></li> <li>Resolves the robot class via <code>constantize.build</code></li> <li>Wires Turbo Stream callbacks when <code>turbo-rails</code> is available (graceful no-op otherwise)</li> <li>Runs the robot and persists the result to <code>RobotLabResult</code></li> <li>Broadcasts completion or error events via Turbo Streams</li> </ol> <p>Customize the generated job to change queue name, retry policy, or error handling.</p>"},{"location":"guides/rails-integration/#turbo-stream-token-streaming","title":"Turbo Stream Token Streaming","text":"<p>When <code>turbo-rails</code> is installed, <code>RobotRunJob</code> automatically streams content tokens and tool call badges to the browser in real time.</p>"},{"location":"guides/rails-integration/#view-setup","title":"View Setup","text":"<p>Subscribe to the thread's Turbo Stream channel in your view:</p> <pre><code>&lt;%%= turbo_stream_from \"robot_lab_thread_#{@thread_id}\" %&gt;\n\n&lt;div id=\"robot_response\"&gt;&lt;/div&gt;\n&lt;div id=\"robot_tools\"&gt;&lt;/div&gt;\n&lt;div id=\"robot_status\"&gt;Processing...&lt;/div&gt;\n&lt;div id=\"robot_errors\"&gt;&lt;/div&gt;\n</code></pre> <p>As the robot generates tokens, they are appended to <code>#robot_response</code>. Tool calls appear as badges in <code>#robot_tools</code>. On completion, <code>#robot_status</code> is replaced with \"Complete\".</p>"},{"location":"guides/rails-integration/#turbostreamcallbacks-api","title":"TurboStreamCallbacks API","text":"<p><code>RobotLab::Rails::TurboStreamCallbacks</code> is a stateless utility module for building callback Procs. Use it outside of <code>RobotRunJob</code> for custom streaming setups:</p> <pre><code># Check if Turbo Streams is available\nRobotLab::Rails::TurboStreamCallbacks.available?\n\n# Build a content streaming callback\non_content = RobotLab::Rails::TurboStreamCallbacks.build_content_callback(\n stream_name: \"robot_lab_thread_#{thread_id}\",\n target: \"robot_response\" # default\n)\n\n# Build a tool call badge callback\non_tool_call = RobotLab::Rails::TurboStreamCallbacks.build_tool_call_callback(\n stream_name: \"robot_lab_thread_#{thread_id}\",\n target: \"robot_tools\" # default\n)\n\n# Wire into a robot at build time\nrobot = SupportRobot.build(on_content: on_content, on_tool_call: on_tool_call)\nrobot.run(message)\n</code></pre> <p>The stream name convention is <code>\"robot_lab_thread_#{thread_id}\"</code>, matching the <code>RobotLabThread.session_id</code> pattern.</p>"},{"location":"guides/rails-integration/#custom-background-job","title":"Custom Background Job","text":"<p>For full control, write your own job instead of using the generated one:</p> app/jobs/process_message_job.rb<pre><code>class ProcessMessageJob &lt; ApplicationJob\n queue_as :default\n\n def perform(session_id:, message:, user_id:)\n robot = SupportRobot.build\n result = robot.run(message)\n\n ActionCable.server.broadcast(\n \"chat_#{session_id}\",\n {\n event: \"complete\",\n response: result.last_text_content,\n robot_name: result.robot_name\n }\n )\n end\nend\n</code></pre>"},{"location":"guides/rails-integration/#testing","title":"Testing","text":""},{"location":"guides/rails-integration/#test-configuration","title":"Test Configuration","text":"<p>Use <code>config/robot_lab.yml</code> to configure the test environment with a faster, cheaper model:</p> config/robot_lab.yml<pre><code>test:\n max_iterations: 3\n streaming_enabled: false\n ruby_llm:\n model: claude-3-haiku-20240307\n request_timeout: 30\n max_retries: 1\n</code></pre>"},{"location":"guides/rails-integration/#robot-tests","title":"Robot Tests","text":"test/robots/support_robot_test.rb<pre><code>require \"test_helper\"\n\nclass SupportRobotTest &lt; ActiveSupport::TestCase\n test \"builds valid robot\" do\n robot = SupportRobot.build\n assert_equal \"support\", robot.name\n end\n\n test \"robot has correct model\" do\n robot = SupportRobot.build\n assert_equal \"claude-sonnet-4\", robot.model\n end\n\n test \"robot has local tools\" do\n robot = SupportRobot.build\n tool_names = robot.local_tools.map(&amp;:name)\n assert_includes tool_names, \"order_lookup\"\n end\nend\n</code></pre>"},{"location":"guides/rails-integration/#integration-tests","title":"Integration Tests","text":"test/integration/chat_test.rb<pre><code>require \"test_helper\"\n\nclass ChatTest &lt; ActionDispatch::IntegrationTest\n test \"processes chat message\" do\n VCR.use_cassette(\"chat_response\") do\n post chat_path, params: { message: \"Hello\" }\n assert_response :success\n\n json = JSON.parse(response.body)\n assert json[\"response\"].present?\n end\n end\nend\n</code></pre>"},{"location":"guides/rails-integration/#models","title":"Models","text":""},{"location":"guides/rails-integration/#thread-model","title":"Thread Model","text":"app/models/robot_lab_thread.rb<pre><code>class RobotLabThread &lt; ApplicationRecord\n has_many :results,\n class_name: \"RobotLabResult\",\n foreign_key: :session_id,\n primary_key: :session_id,\n dependent: :destroy\n\n validates :session_id, presence: true, uniqueness: true\n\n def self.find_or_create_by_session_id(id)\n find_or_create_by(session_id: id)\n end\n\n def last_result\n results.order(sequence_number: :desc).first\n end\nend\n</code></pre>"},{"location":"guides/rails-integration/#result-model","title":"Result Model","text":"app/models/robot_lab_result.rb<pre><code>class RobotLabResult &lt; ApplicationRecord\n belongs_to :thread,\n class_name: \"RobotLabThread\",\n foreign_key: :session_id,\n primary_key: :session_id\n\n validates :session_id, presence: true\n validates :robot_name, presence: true\n\n default_scope { order(sequence_number: :asc) }\n\n def to_robot_result\n RobotLab::RobotResult.new(\n robot_name: robot_name,\n output: (output_data || []).map { |d| RobotLab::Message.from_hash(d.symbolize_keys) },\n tool_calls: (tool_calls_data || []).map { |d| RobotLab::Message.from_hash(d.symbolize_keys) },\n stop_reason: stop_reason\n )\n end\nend\n</code></pre>"},{"location":"guides/rails-integration/#best-practices","title":"Best Practices","text":""},{"location":"guides/rails-integration/#1-use-service-objects","title":"1. Use Service Objects","text":"app/services/chat_service.rb<pre><code>class ChatService\n def initialize(user:)\n @user = user\n end\n\n def process(message)\n robot = SupportRobot.build\n result = robot.run(message)\n\n {\n response: result.last_text_content,\n robot_name: result.robot_name\n }\n end\n\n def process_with_network(message)\n support_robot = SupportRobot.build\n billing_robot = BillingRobot.build\n\n network = RobotLab.create_network(name: \"customer_service\") do\n task :support, support_robot, depends_on: :none\n task :billing, billing_robot, depends_on: :optional\n end\n\n result = network.run(message: message, user_id: @user.id)\n\n {\n response: result.value.last_text_content,\n robot_name: result.value.robot_name\n }\n end\nend\n</code></pre>"},{"location":"guides/rails-integration/#2-handle-errors","title":"2. Handle Errors","text":"<pre><code>def create\n result = ChatService.new(user: current_user).process(params[:message])\n render json: result\nrescue RobotLab::Error =&gt; e\n render json: { error: e.message }, status: :unprocessable_entity\nrescue StandardError =&gt; e\n Rails.logger.error(\"Chat error: #{e.message}\")\n render json: { error: \"An error occurred\" }, status: :internal_server_error\nend\n</code></pre>"},{"location":"guides/rails-integration/#3-rate-limiting","title":"3. Rate Limiting","text":"<pre><code>class ChatController &lt; ApplicationController\n before_action :check_rate_limit\n\n private\n\n def check_rate_limit\n key = \"chat_rate:#{current_user.id}\"\n count = Rails.cache.increment(key, 1, expires_in: 1.minute)\n\n if count &gt; 10\n render json: { error: \"Rate limit exceeded\" }, status: :too_many_requests\n end\n end\nend\n</code></pre>"},{"location":"guides/rails-integration/#next-steps","title":"Next Steps","text":"<ul> <li>Building Robots - Robot patterns</li> <li>Creating Networks - Network configuration</li> </ul>"},{"location":"guides/streaming/","title":"Streaming Responses","text":"<p>Stream LLM responses in real-time for better user experience.</p>"},{"location":"guides/streaming/#streaming-via-callbacks","title":"Streaming via Callbacks","text":"<p>RobotLab robots support streaming through callback methods inherited from RubyLLM::Agent. Register callbacks before calling <code>run</code>:</p> <pre><code>robot = RobotLab.build(\n name: \"storyteller\",\n system_prompt: \"You are a creative storyteller.\"\n)\n\n# Register streaming callback\nrobot.on_new_message do |message|\n print message.content if message.content\nend\n\nresult = robot.run(\"Tell me a story about a brave robot\")\n</code></pre>"},{"location":"guides/streaming/#available-callbacks","title":"Available Callbacks","text":""},{"location":"guides/streaming/#on_new_message","title":"on_new_message","text":"<p>Called when the assistant starts generating a new message, with streaming chunks:</p> <pre><code>robot.on_new_message do |message|\n print message.content if message.content\nend\n</code></pre>"},{"location":"guides/streaming/#on_end_message","title":"on_end_message","text":"<p>Called when the assistant finishes a message:</p> <pre><code>robot.on_end_message do |message|\n puts \"\\n--- Response complete ---\"\n puts \"Content length: #{message.content&amp;.length}\"\nend\n</code></pre>"},{"location":"guides/streaming/#on_tool_call","title":"on_tool_call","text":"<p>Called when the LLM invokes a tool:</p> <pre><code>robot.on_tool_call do |tool_call|\n puts \"Calling tool: #{tool_call.name}\"\nend\n</code></pre>"},{"location":"guides/streaming/#on_tool_result","title":"on_tool_result","text":"<p>Called when a tool returns its result:</p> <pre><code>robot.on_tool_result do |tool_call, result|\n puts \"Tool #{tool_call.name} returned: #{result}\"\nend\n</code></pre>"},{"location":"guides/streaming/#comprehensive-callback-setup","title":"Comprehensive Callback Setup","text":"<p>Register all callbacks for full visibility:</p> <pre><code>robot = RobotLab.build(\n name: \"assistant\",\n system_prompt: \"You are helpful.\",\n local_tools: [WeatherTool]\n)\n\nrobot.on_new_message do |message|\n print message.content if message.content\nend\n\nrobot.on_end_message do |_message|\n puts \"\\n--- Done ---\"\nend\n\nrobot.on_tool_call do |tool_call|\n puts \"\\n[Tool] Calling: #{tool_call.name}\"\nend\n\nrobot.on_tool_result do |tool_call, result|\n puts \"[Tool] #{tool_call.name} returned: #{result}\"\nend\n\nresult = robot.run(\"What's the weather in Tokyo?\")\n</code></pre>"},{"location":"guides/streaming/#streaming-via-chat-block","title":"Streaming via Chat Block","text":"<p>For more control, pass a block directly to <code>chat.ask</code> (the underlying RubyLLM method):</p> <pre><code>robot = RobotLab.build(\n name: \"chat_bot\",\n system_prompt: \"You are a helpful assistant.\"\n)\n\n# Use the underlying chat directly with a streaming block\nrobot.chat.ask(\"Tell me a story\") do |chunk|\n print chunk.content if chunk.content\nend\n</code></pre> <p>Note: Using <code>chat.ask</code> directly bypasses Robot's memory resolution and tool hierarchy. Use callbacks with <code>robot.run</code> when you need those features.</p>"},{"location":"guides/streaming/#web-integration","title":"Web Integration","text":""},{"location":"guides/streaming/#rails-action-cable","title":"Rails Action Cable","text":"<pre><code>class ChatChannel &lt; ApplicationCable::Channel\n def receive(data)\n robot = RobotLab.build(\n name: \"chat_bot\",\n system_prompt: \"You are a helpful chat assistant.\"\n )\n\n robot.on_new_message do |message|\n transmit({ event: \"text.delta\", content: message.content }) if message.content\n end\n\n robot.on_end_message do |_message|\n transmit({ event: \"run.completed\" })\n end\n\n robot.run(data[\"message\"])\n end\nend\n</code></pre>"},{"location":"guides/streaming/#server-sent-events","title":"Server-Sent Events","text":"<pre><code>class StreamController &lt; ApplicationController\n include ActionController::Live\n\n def create\n response.headers[\"Content-Type\"] = \"text/event-stream\"\n\n robot = RobotLab.build(\n name: \"stream_bot\",\n system_prompt: \"You are helpful.\"\n )\n\n robot.on_new_message do |message|\n response.stream.write(\"data: #{message.content}\\n\\n\") if message.content\n end\n\n robot.on_end_message do |_message|\n response.stream.write(\"data: [DONE]\\n\\n\")\n end\n\n robot.run(params[:message])\n ensure\n response.stream.close\n end\nend\n</code></pre>"},{"location":"guides/streaming/#websocket","title":"WebSocket","text":"<pre><code># Using Faye WebSocket\nws.on :message do |msg|\n robot.on_new_message do |message|\n ws.send(message.content) if message.content\n end\n\n robot.run(msg.data)\nend\n</code></pre>"},{"location":"guides/streaming/#progress-tracking","title":"Progress Tracking","text":"<p>Track streaming progress with callbacks:</p> <pre><code>class StreamProgress\n def initialize\n @chars = 0\n @tools = 0\n end\n\n attr_reader :chars, :tools\n\n def attach(robot)\n robot.on_new_message do |message|\n @chars += message.content.length if message.content\n print \"\\rReceived #{@chars} characters...\"\n end\n\n robot.on_tool_call do |tool_call|\n @tools += 1\n puts \"\\nTool call ##{@tools}: #{tool_call.name}\"\n end\n end\nend\n\nprogress = StreamProgress.new\nprogress.attach(robot)\n\nresult = robot.run(\"Process this complex request\")\nputs \"\\nTotal: #{progress.chars} chars, #{progress.tools} tool calls\"\n</code></pre>"},{"location":"guides/streaming/#without-streaming","title":"Without Streaming","text":"<p>When streaming is not needed, simply call <code>run</code> without registering callbacks:</p> <pre><code># No streaming - returns RobotResult directly\nresult = robot.run(\"Hello!\")\nputs result.last_text_content\n</code></pre>"},{"location":"guides/streaming/#best-practices","title":"Best Practices","text":""},{"location":"guides/streaming/#1-register-callbacks-before-run","title":"1. Register Callbacks Before Run","text":"<pre><code># Correct: register first, then run\nrobot.on_new_message { |msg| print msg.content if msg.content }\nrobot.run(\"Hello\")\n</code></pre>"},{"location":"guides/streaming/#2-handle-errors-in-callbacks","title":"2. Handle Errors in Callbacks","text":"<pre><code>robot.on_new_message do |message|\n begin\n broadcast(message.content) if message.content\n rescue BroadcastError =&gt; e\n # Client disconnected, but continue processing\n logger.warn \"Broadcast failed: #{e.message}\"\n end\nend\n</code></pre>"},{"location":"guides/streaming/#3-clean-up-resources","title":"3. Clean Up Resources","text":"<pre><code>begin\n robot.on_new_message do |message|\n stream_to_client(message.content) if message.content\n end\n robot.run(\"Hello\")\nensure\n close_stream_connection\nend\n</code></pre>"},{"location":"guides/streaming/#next-steps","title":"Next Steps","text":"<ul> <li>Building Robots - Robot creation</li> <li>Creating Networks - Network patterns</li> <li>API Reference: Streaming - Complete API</li> </ul>"},{"location":"guides/using-tools/","title":"Using Tools","text":"<p>Tools give robots the ability to interact with external systems. RobotLab supports three approaches: <code>RubyLLM::Tool</code> subclasses, <code>RobotLab::Tool</code> subclasses (with robot access), and <code>RobotLab::Tool.create</code> for dynamic tools.</p>"},{"location":"guides/using-tools/#defining-tools","title":"Defining Tools","text":""},{"location":"guides/using-tools/#rubyllmtool-subclass","title":"RubyLLM::Tool Subclass","text":"<p>For reusable tools that don't need robot access:</p> <pre><code>class GetWeather &lt; RubyLLM::Tool\n description \"Get current weather for a location\"\n\n param :location, type: :string, desc: \"City name or zip code\"\n param :unit, type: :string, desc: \"Temperature unit\", required: false\n\n def execute(location:, unit: \"celsius\")\n WeatherService.current(location, unit: unit)\n end\nend\n</code></pre>"},{"location":"guides/using-tools/#robotlabtool-subclass","title":"RobotLab::Tool Subclass","text":"<p>For tools that need access to their owning robot (self-modification, spawning, etc.):</p> <pre><code>class AdjustEnergy &lt; RobotLab::Tool\n description \"Adjust the robot's creativity level\"\n\n param :level, type: \"number\", desc: \"Temperature from 0.0 to 1.0\"\n\n def execute(level:)\n robot.with_temperature(level)\n \"Temperature adjusted to #{level}\"\n end\nend\n</code></pre> <p>Pass <code>robot: self</code> when constructing:</p> <pre><code>class MyRobot &lt; RobotLab::Robot\n def initialize\n super(\n name: \"creative_bot\",\n system_prompt: \"You are creative.\",\n local_tools: [AdjustEnergy.new(robot: self)]\n )\n end\nend\n</code></pre>"},{"location":"guides/using-tools/#robotlabtoolcreate-dynamic-tools","title":"RobotLab::Tool.create (Dynamic Tools)","text":"<p>For quick, inline tools use the <code>Tool.create</code> factory:</p> <pre><code>get_time = RobotLab::Tool.create(\n name: \"get_time\",\n description: \"Get the current time\"\n) { |_args| Time.now.to_s }\n</code></pre> <p>With parameters:</p> <pre><code>weather_tool = RobotLab::Tool.create(\n name: \"get_weather\",\n description: \"Get current weather for a location\",\n parameters: {\n type: \"object\",\n properties: {\n location: { type: \"string\", description: \"City name\" }\n },\n required: [\"location\"]\n }\n) { |args| WeatherService.current(args[:location]) }\n</code></pre>"},{"location":"guides/using-tools/#built-in-tools","title":"Built-in Tools","text":""},{"location":"guides/using-tools/#askuser","title":"AskUser","text":"<p><code>RobotLab::AskUser</code> lets a robot ask the user a question via the terminal. The LLM decides when it needs human input and calls the tool with a question, optional choices, and an optional default.</p> <pre><code>robot = RobotLab.build(\n name: \"onboarding\",\n system_prompt: \"Walk the user through project setup. Ask questions to understand their needs.\",\n local_tools: [RobotLab::AskUser]\n)\nrobot.run(\"Help the user set up a new project\")\n</code></pre> <p>The tool displays the robot's name and question, then waits for terminal input:</p> <pre><code>[onboarding] What programming language will you use?\n 1. Ruby\n 2. Python\n 3. Go\n&gt;\n</code></pre> <p>Features:</p> <ul> <li>Open-ended: just a question, free-text response</li> <li>Multiple choice: numbered options, user types the number or text</li> <li>Default value: shown in the prompt, used when user presses Enter</li> </ul> <p>IO is sourced from <code>robot.input</code> / <code>robot.output</code> (defaulting to <code>$stdin</code> / <code>$stdout</code>), making it easy to test with <code>StringIO</code>:</p> <pre><code>robot.input = StringIO.new(\"2\\n\")\nrobot.output = StringIO.new\n</code></pre> <p>See the AskUser API reference for full details.</p>"},{"location":"guides/using-tools/#attaching-tools-to-robots","title":"Attaching Tools to Robots","text":""},{"location":"guides/using-tools/#via-constructor","title":"Via Constructor","text":"<p>Pass tools via the <code>local_tools:</code> parameter when building a robot:</p> <pre><code>robot = RobotLab.build(\n name: \"assistant\",\n system_prompt: \"You are a helpful assistant with tool access.\",\n local_tools: [GetWeather, CalculatorTool]\n)\n</code></pre>"},{"location":"guides/using-tools/#via-template-front-matter","title":"Via Template Front Matter","text":"<p>Declare tool class names in the template's YAML front matter. RobotLab resolves each string to a Ruby constant via <code>Object.const_get</code> and instantiates it:</p> prompts/weather_bot.md<pre><code>---\ndescription: Weather assistant with forecast tools\ntools:\n - GetWeather\n - GetForecast\n---\nYou are a weather assistant. Use your tools to look up weather information.\n</code></pre> <pre><code># Tools are resolved from frontmatter \u2014 no local_tools: needed\nrobot = RobotLab.build(template: :weather_bot)\n</code></pre> <p>Tool classes must be defined and loaded before building the robot. Unresolvable names are skipped with a warning. Constructor <code>local_tools:</code> overrides frontmatter <code>tools:</code> when provided.</p>"},{"location":"guides/using-tools/#via-chaining","title":"Via Chaining","text":"<p>You can also add tools dynamically with chaining:</p> <pre><code>robot = RobotLab.build(name: \"assistant\", system_prompt: \"...\")\nrobot.with_tools(GetWeather, CalculatorTool)\n</code></pre>"},{"location":"guides/using-tools/#parameter-types","title":"Parameter Types","text":"<p>Define parameters on <code>RubyLLM::Tool</code> subclasses using <code>param</code>:</p>"},{"location":"guides/using-tools/#string","title":"String","text":"<pre><code>param :name, type: :string, desc: \"User's full name\"\n</code></pre>"},{"location":"guides/using-tools/#integer","title":"Integer","text":"<pre><code>param :count, type: :integer, desc: \"Number of results\"\n</code></pre>"},{"location":"guides/using-tools/#number-float","title":"Number (Float)","text":"<pre><code>param :price, type: :number, desc: \"Price in dollars\"\n</code></pre>"},{"location":"guides/using-tools/#boolean","title":"Boolean","text":"<pre><code>param :active, type: :boolean, desc: \"Whether the user is active\"\n</code></pre>"},{"location":"guides/using-tools/#array","title":"Array","text":"<pre><code>param :tags, type: :array, desc: \"List of tags\"\n</code></pre>"},{"location":"guides/using-tools/#enum","title":"Enum","text":"<pre><code>param :status, type: :string, desc: \"Order status\", enum: %w[pending active completed]\n</code></pre>"},{"location":"guides/using-tools/#required-vs-optional","title":"Required vs Optional","text":"<p>Parameters are required by default. Mark optional with <code>required: false</code>:</p> <pre><code>param :query, type: :string, desc: \"Search query\" # required\nparam :limit, type: :integer, desc: \"Max results\", required: false # optional\n</code></pre>"},{"location":"guides/using-tools/#tool-patterns","title":"Tool Patterns","text":""},{"location":"guides/using-tools/#database-lookup","title":"Database Lookup","text":"<pre><code>class FindUser &lt; RubyLLM::Tool\n description \"Find user by email or ID\"\n\n param :identifier, type: :string, desc: \"Email address or user ID\"\n\n def execute(identifier:)\n user = User.find_by(id: identifier) || User.find_by(email: identifier)\n user ? user.to_h : { error: \"User not found\" }\n end\nend\n</code></pre>"},{"location":"guides/using-tools/#api-integration","title":"API Integration","text":"<pre><code>class GetStockPrice &lt; RubyLLM::Tool\n description \"Get current stock price for a ticker symbol\"\n\n param :symbol, type: :string, desc: \"Stock ticker symbol (e.g. AAPL)\"\n\n def execute(symbol:)\n response = HTTP.get(\"https://api.stocks.example/quote/#{symbol}\")\n JSON.parse(response.body)\n rescue HTTP::Error =&gt; e\n { error: \"Failed to fetch stock price: #{e.message}\" }\n end\nend\n</code></pre>"},{"location":"guides/using-tools/#file-operations","title":"File Operations","text":"<pre><code>class ReadFile &lt; RubyLLM::Tool\n description \"Read contents of a file\"\n\n param :path, type: :string, desc: \"Absolute path to the file\"\n\n def execute(path:)\n if File.exist?(path) &amp;&amp; File.readable?(path)\n { content: File.read(path), size: File.size(path) }\n else\n { error: \"File not found or not readable\" }\n end\n end\nend\n</code></pre>"},{"location":"guides/using-tools/#multi-step-operations","title":"Multi-Step Operations","text":"<pre><code>class ProcessOrder &lt; RubyLLM::Tool\n description \"Validate and process a customer order\"\n\n param :order_id, type: :string, desc: \"The order ID to process\"\n\n def execute(order_id:)\n order = Order.find(order_id)\n\n # Validate\n return { error: \"Invalid order\" } unless order.valid?\n\n # Process payment\n result = PaymentProcessor.charge(order)\n return { error: result[:error] } unless result[:success]\n\n # Update status\n order.update!(status: \"paid\")\n\n { success: true, order_id: order.id, amount: order.total }\n end\nend\n</code></pre>"},{"location":"guides/using-tools/#tool-return-values","title":"Tool Return Values","text":""},{"location":"guides/using-tools/#structured-data","title":"Structured Data","text":"<p>Return hashes with consistent structure:</p> <pre><code>def execute(user_id:)\n user = User.find(user_id)\n {\n id: user.id,\n name: user.name,\n email: user.email,\n created_at: user.created_at.iso8601\n }\nend\n</code></pre>"},{"location":"guides/using-tools/#simple-values","title":"Simple Values","text":"<pre><code>def execute(**_)\n Time.now.to_s\nend\n</code></pre>"},{"location":"guides/using-tools/#lists","title":"Lists","text":"<pre><code>def execute(query:)\n results = Search.query(query)\n results.map { |r| { id: r.id, title: r.title, score: r.score } }\nend\n</code></pre>"},{"location":"guides/using-tools/#error-handling","title":"Error Handling","text":""},{"location":"guides/using-tools/#automatic-error-handling","title":"Automatic Error Handling","text":"<p><code>RobotLab::Tool</code> automatically catches <code>StandardError</code> exceptions from <code>execute</code> and returns a plain-text error string to the LLM. The LLM can then reason about the failure and try an alternative approach \u2014 without crashing the run.</p> <pre><code>class FetchResource &lt; RobotLab::Tool\n description \"Fetch a resource from an external API\"\n param :id, type: :string, desc: \"Resource ID\"\n\n def execute(id:)\n ExternalAPI.fetch(id)\n end\nend\n\ntool = FetchResource.new\nresult = tool.call({ \"id\" =&gt; \"missing\" })\n# If ExternalAPI.fetch raises, result is:\n# =&gt; \"Error (fetch_resource): connection refused\"\n</code></pre> <p>This applies to all <code>RobotLab::Tool</code> variants \u2014 subclasses, <code>Tool.create</code> factory tools, and MCP tools. Errors are also logged via <code>RobotLab.config.logger</code> at <code>:warn</code> level.</p>"},{"location":"guides/using-tools/#critical-tools-opt-out","title":"Critical Tools (Opt-Out)","text":"<p>For tools where you want exceptions to propagate (e.g., a tool whose failure should abort the run), set <code>raise_on_error</code> on the class:</p> <pre><code>class CriticalPayment &lt; RobotLab::Tool\n self.raise_on_error = true\n\n description \"Process a payment\"\n param :amount, type: :number, desc: \"Payment amount\"\n\n def execute(amount:)\n PaymentGateway.charge(amount)\n end\nend\n</code></pre> <p><code>raise_on_error</code> is per-class and defaults to <code>false</code>. Setting it on one class does not affect others.</p>"},{"location":"guides/using-tools/#manual-error-handling","title":"Manual Error Handling","text":"<p>You can still handle specific errors inside <code>execute</code> for domain-specific responses:</p> <pre><code>class FetchResource &lt; RobotLab::Tool\n description \"Fetch a resource from an external API\"\n param :id, type: :string, desc: \"Resource ID\"\n\n def execute(id:)\n result = ExternalAPI.fetch(id)\n { success: true, data: result }\n rescue ExternalAPI::NotFound\n { success: false, error: \"Resource not found\", id: id }\n rescue ExternalAPI::RateLimited =&gt; e\n { success: false, error: \"Rate limited\", retry_after: e.retry_after }\n end\n # Any other StandardError is still caught by the automatic handler\nend\n</code></pre>"},{"location":"guides/using-tools/#tool-callbacks","title":"Tool Callbacks","text":"<p>Robots support <code>on_tool_call</code> and <code>on_tool_result</code> callbacks for monitoring tool usage:</p> <pre><code>robot = RobotLab.build(\n name: \"assistant\",\n system_prompt: \"...\",\n local_tools: [GetWeather],\n on_tool_call: -&gt;(call) { puts \"Calling: #{call}\" },\n on_tool_result: -&gt;(result) { puts \"Result: #{result}\" }\n)\n</code></pre>"},{"location":"guides/using-tools/#robotlabtoolcreate-with-schema","title":"RobotLab::Tool.create with Schema","text":"<p>For dynamic tools via <code>Tool.create</code>, pass parameters as a JSON Schema hash:</p> <pre><code>tool = RobotLab::Tool.create(\n name: \"search\",\n description: \"Search for items\",\n parameters: {\n type: \"object\",\n properties: {\n query: { type: \"string\", description: \"Search query\" },\n limit: { type: \"integer\", description: \"Max results\" }\n },\n required: [\"query\"]\n }\n) { |args| Search.query(args[:query], limit: args[:limit] || 10) }\n</code></pre>"},{"location":"guides/using-tools/#best-practices","title":"Best Practices","text":""},{"location":"guides/using-tools/#1-clear-descriptions","title":"1. Clear Descriptions","text":"<p>Write descriptions that help the LLM understand when and how to use the tool:</p> <pre><code># Good: Specific and actionable\nclass SearchOrders &lt; RubyLLM::Tool\n description \"Search customer orders by date range, status, or customer email. Returns up to 50 matching orders sorted by date.\"\n # ...\nend\n\n# Bad: Vague\nclass Search &lt; RubyLLM::Tool\n description \"Searches stuff\"\n # ...\nend\n</code></pre>"},{"location":"guides/using-tools/#2-validate-inputs","title":"2. Validate Inputs","text":"<pre><code>def execute(email:)\n unless email.match?(/\\A[\\w+\\-.]+@[a-z\\d\\-]+(\\.[a-z\\d\\-]+)*\\.[a-z]+\\z/i)\n return { error: \"Invalid email format\" }\n end\n # ... rest of logic\nend\n</code></pre>"},{"location":"guides/using-tools/#3-return-structured-data","title":"3. Return Structured Data","text":"<pre><code># Good: Structured and consistent\ndef execute(**_)\n {\n success: true,\n data: { id: 1, name: \"Item\" },\n metadata: { fetched_at: Time.now.iso8601 }\n }\nend\n\n# Bad: Unstructured\ndef execute(**_)\n \"Found item with id 1 named Item\"\nend\n</code></pre>"},{"location":"guides/using-tools/#4-keep-tools-focused","title":"4. Keep Tools Focused","text":"<p>Each tool should do one thing well. Prefer multiple focused tools over one tool that does everything.</p>"},{"location":"guides/using-tools/#next-steps","title":"Next Steps","text":"<ul> <li>MCP Integration - External tool servers</li> <li>Building Robots - Robot creation patterns</li> <li>API Reference: Tool - Complete API</li> </ul>"}]}