robot_lab 0.2.8 → 0.3.1

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