robot_lab 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/README.md +29 -19
  4. data/lib/robot_lab/errors.rb +4 -0
  5. data/lib/robot_lab/hook_context.rb +9 -2
  6. data/lib/robot_lab/mcp/registry.rb +99 -0
  7. data/lib/robot_lab/mcp/server.rb +47 -83
  8. data/lib/robot_lab/mcp/server_discovery.rb +22 -9
  9. data/lib/robot_lab/mcp.rb +259 -0
  10. data/lib/robot_lab/robot/mcp_management.rb +159 -105
  11. data/lib/robot_lab/robot/tool_call_gating.rb +144 -0
  12. data/lib/robot_lab/robot.rb +72 -99
  13. data/lib/robot_lab/run_config.rb +3 -2
  14. data/lib/robot_lab/tool.rb +8 -23
  15. data/lib/robot_lab/version.rb +1 -1
  16. data/lib/robot_lab.rb +1 -4
  17. metadata +16 -292
  18. data/.architecture/AGENTS.md +0 -32
  19. data/.architecture/config.yml +0 -8
  20. data/.architecture/members.yml +0 -60
  21. data/.architecture/reviews/feature-free-will.md +0 -490
  22. data/.architecture/reviews/overall-codebase.md +0 -427
  23. data/.claude/settings.local.json +0 -57
  24. data/.codex/config.toml +0 -2
  25. data/.envrc +0 -6
  26. data/.github/workflows/deploy-github-pages.yml +0 -52
  27. data/.irbrc +0 -6
  28. data/.loki +0 -65
  29. data/.quality/reek_baseline.txt +0 -43
  30. data/.rubocop.yml +0 -10
  31. data/Archspec.rb +0 -44
  32. data/CLAUDE.md +0 -141
  33. data/COMMITS.md +0 -196
  34. data/Rakefile +0 -193
  35. data/_typos.toml +0 -21
  36. data/agent2agent_review.md +0 -192
  37. data/agentf_improvements.md +0 -253
  38. data/agents.md +0 -14
  39. data/docs/api/core/index.md +0 -147
  40. data/docs/api/core/memory.md +0 -578
  41. data/docs/api/core/network.md +0 -553
  42. data/docs/api/core/result.md +0 -211
  43. data/docs/api/core/robot.md +0 -1667
  44. data/docs/api/core/state.md +0 -145
  45. data/docs/api/core/tool.md +0 -520
  46. data/docs/api/errors.md +0 -323
  47. data/docs/api/hooks.md +0 -469
  48. data/docs/api/index.md +0 -137
  49. data/docs/api/mcp/client.md +0 -387
  50. data/docs/api/mcp/index.md +0 -267
  51. data/docs/api/mcp/server.md +0 -252
  52. data/docs/api/mcp/transports.md +0 -339
  53. data/docs/api/messages/index.md +0 -126
  54. data/docs/api/messages/text-message.md +0 -170
  55. data/docs/api/messages/tool-call-message.md +0 -206
  56. data/docs/api/messages/tool-result-message.md +0 -242
  57. data/docs/api/messages/user-message.md +0 -232
  58. data/docs/api/skills.md +0 -423
  59. data/docs/api/streaming/context.md +0 -280
  60. data/docs/api/streaming/events.md +0 -185
  61. data/docs/api/streaming/index.md +0 -156
  62. data/docs/api/support.md +0 -846
  63. data/docs/architecture/core-concepts.md +0 -575
  64. data/docs/architecture/index.md +0 -252
  65. data/docs/architecture/message-flow.md +0 -366
  66. data/docs/architecture/network-orchestration.md +0 -478
  67. data/docs/architecture/robot-execution.md +0 -396
  68. data/docs/architecture/state-management.md +0 -410
  69. data/docs/assets/css/custom.css +0 -56
  70. data/docs/assets/images/robot_lab.jpg +0 -0
  71. data/docs/concepts.md +0 -641
  72. data/docs/examples/basic-chat.md +0 -258
  73. data/docs/examples/index.md +0 -425
  74. data/docs/examples/mcp-server.md +0 -403
  75. data/docs/examples/multi-robot-network.md +0 -476
  76. data/docs/examples/tool-usage.md +0 -468
  77. data/docs/getting-started/configuration.md +0 -645
  78. data/docs/getting-started/index.md +0 -56
  79. data/docs/getting-started/installation.md +0 -197
  80. data/docs/getting-started/quick-start.md +0 -293
  81. data/docs/guides/building-robots.md +0 -1187
  82. data/docs/guides/creating-networks.md +0 -736
  83. data/docs/guides/hooks.md +0 -1206
  84. data/docs/guides/index.md +0 -78
  85. data/docs/guides/knowledge.md +0 -219
  86. data/docs/guides/mcp-integration.md +0 -740
  87. data/docs/guides/memory.md +0 -458
  88. data/docs/guides/observability.md +0 -733
  89. data/docs/guides/streaming.md +0 -323
  90. data/docs/guides/using-tools.md +0 -780
  91. data/docs/index.md +0 -280
  92. data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
  93. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
  94. data/examples/.envrc +0 -3
  95. data/examples/01_simple_robot.rb +0 -31
  96. data/examples/02_tools.rb +0 -109
  97. data/examples/03_network.rb +0 -118
  98. data/examples/04_mcp.rb +0 -217
  99. data/examples/05_streaming.rb +0 -137
  100. data/examples/06_prompt_templates.rb +0 -325
  101. data/examples/07_network_memory.rb +0 -313
  102. data/examples/08_llm_config.rb +0 -162
  103. data/examples/09_chaining.rb +0 -251
  104. data/examples/10_memory.rb +0 -315
  105. data/examples/11_network_introspection.rb +0 -239
  106. data/examples/12_message_bus.rb +0 -67
  107. data/examples/13_spawn.rb +0 -85
  108. data/examples/14_rusty_circuit/.envrc +0 -1
  109. data/examples/14_rusty_circuit/.gitignore +0 -1
  110. data/examples/14_rusty_circuit/README.md +0 -1
  111. data/examples/14_rusty_circuit/comic.rb +0 -149
  112. data/examples/14_rusty_circuit/display.rb +0 -217
  113. data/examples/14_rusty_circuit/heckler.rb +0 -65
  114. data/examples/14_rusty_circuit/open_mic.rb +0 -132
  115. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
  116. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
  117. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
  118. data/examples/14_rusty_circuit/scout.rb +0 -165
  119. data/examples/14_rusty_circuit/show.log +0 -234
  120. data/examples/15_memory_network_and_bus/.envrc +0 -1
  121. data/examples/15_memory_network_and_bus/README.md +0 -66
  122. data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
  123. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
  124. data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
  125. data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
  126. data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
  127. data/examples/15_memory_network_and_bus/output/combined_article.md +0 -11
  128. data/examples/15_memory_network_and_bus/output/final_article.md +0 -8
  129. data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
  130. data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
  131. data/examples/15_memory_network_and_bus/output/memory.json +0 -13
  132. data/examples/15_memory_network_and_bus/output/revision_1.md +0 -29
  133. data/examples/15_memory_network_and_bus/output/revision_2.md +0 -50
  134. data/examples/15_memory_network_and_bus/output/revision_3.md +0 -8
  135. data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
  136. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
  137. data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
  138. data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
  139. data/examples/16_writers_room/.envrc +0 -1
  140. data/examples/16_writers_room/display.rb +0 -158
  141. data/examples/16_writers_room/output/.gitignore +0 -4
  142. data/examples/16_writers_room/output/README.md +0 -69
  143. data/examples/16_writers_room/output/opus_001.md +0 -263
  144. data/examples/16_writers_room/output/opus_001_notes.log +0 -470
  145. data/examples/16_writers_room/output/opus_002.md +0 -245
  146. data/examples/16_writers_room/output/opus_002_notes.log +0 -546
  147. data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
  148. data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
  149. data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
  150. data/examples/16_writers_room/prompts/writer.md +0 -37
  151. data/examples/16_writers_room/room.rb +0 -186
  152. data/examples/16_writers_room/tools.rb +0 -173
  153. data/examples/16_writers_room/writer.rb +0 -121
  154. data/examples/16_writers_room/writers_room.rb +0 -256
  155. data/examples/17_skills.rb +0 -234
  156. data/examples/18_rails/.envrc +0 -3
  157. data/examples/18_rails/.gitignore +0 -5
  158. data/examples/18_rails/Gemfile +0 -11
  159. data/examples/18_rails/README.md +0 -67
  160. data/examples/18_rails/Rakefile +0 -4
  161. data/examples/18_rails/app/controllers/application_controller.rb +0 -4
  162. data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
  163. data/examples/18_rails/app/jobs/application_job.rb +0 -4
  164. data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
  165. data/examples/18_rails/app/models/application_record.rb +0 -5
  166. data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
  167. data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
  168. data/examples/18_rails/app/robots/chat_robot.rb +0 -25
  169. data/examples/18_rails/app/tools/time_tool.rb +0 -9
  170. data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
  171. data/examples/18_rails/app/views/chat/index.html.erb +0 -67
  172. data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
  173. data/examples/18_rails/bin/dev +0 -7
  174. data/examples/18_rails/bin/rails +0 -6
  175. data/examples/18_rails/bin/setup +0 -15
  176. data/examples/18_rails/config/application.rb +0 -33
  177. data/examples/18_rails/config/cable.yml +0 -2
  178. data/examples/18_rails/config/database.yml +0 -5
  179. data/examples/18_rails/config/environment.rb +0 -4
  180. data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
  181. data/examples/18_rails/config/routes.rb +0 -6
  182. data/examples/18_rails/config.ru +0 -4
  183. data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
  184. data/examples/19_token_tracking.rb +0 -138
  185. data/examples/20_circuit_breaker.rb +0 -147
  186. data/examples/21_learning_loop.rb +0 -181
  187. data/examples/22_context_compression.rb +0 -172
  188. data/examples/23_convergence.rb +0 -133
  189. data/examples/24_structured_delegation.rb +0 -151
  190. data/examples/25_history_search/conversation.jsonl +0 -30
  191. data/examples/25_history_search.rb +0 -133
  192. data/examples/26_document_store/api_versioning_adr.md +0 -52
  193. data/examples/26_document_store/incident_postmortem.md +0 -46
  194. data/examples/26_document_store/postgres_runbook.md +0 -49
  195. data/examples/26_document_store/redis_caching_guide.md +0 -48
  196. data/examples/26_document_store/sidekiq_guide.md +0 -51
  197. data/examples/26_document_store.rb +0 -140
  198. data/examples/27_incident_response/README.md +0 -65
  199. data/examples/27_incident_response/incident_response.rb +0 -261
  200. data/examples/28_mcp_discovery.rb +0 -113
  201. data/examples/29_ractor_tools.rb +0 -240
  202. data/examples/30_ractor_network.rb +0 -252
  203. data/examples/31_launch_assessment.rb +0 -246
  204. data/examples/32_newsletter_reader.rb +0 -193
  205. data/examples/33_stock_generator.rb +0 -80
  206. data/examples/33_stock_predictor.rb +0 -327
  207. data/examples/34_agentskills.rb +0 -76
  208. data/examples/35_hooks.rb +0 -266
  209. data/examples/README.md +0 -550
  210. data/examples/common.rb +0 -180
  211. data/examples/prompts/assistant.md +0 -5
  212. data/examples/prompts/audit_trail.md +0 -8
  213. data/examples/prompts/billing.md +0 -10
  214. data/examples/prompts/classifier.md +0 -7
  215. data/examples/prompts/comedian.md +0 -6
  216. data/examples/prompts/comedy_critic.md +0 -10
  217. data/examples/prompts/configurable.md +0 -9
  218. data/examples/prompts/dispatcher.md +0 -12
  219. data/examples/prompts/entity_extractor.md +0 -14
  220. data/examples/prompts/escalation.md +0 -42
  221. data/examples/prompts/frontmatter_mcp_test.md +0 -9
  222. data/examples/prompts/frontmatter_named_test.md +0 -5
  223. data/examples/prompts/frontmatter_tools_test.md +0 -6
  224. data/examples/prompts/general.md +0 -7
  225. data/examples/prompts/github_assistant.md +0 -14
  226. data/examples/prompts/helper.md +0 -4
  227. data/examples/prompts/incident_responder.md +0 -18
  228. data/examples/prompts/keyword_extractor.md +0 -11
  229. data/examples/prompts/llm_config_demo.md +0 -20
  230. data/examples/prompts/order_support.md +0 -35
  231. data/examples/prompts/os_advocate.md +0 -13
  232. data/examples/prompts/os_chief.md +0 -13
  233. data/examples/prompts/os_editor.md +0 -13
  234. data/examples/prompts/parameterized_main_test.md +0 -6
  235. data/examples/prompts/pii_redactor.md +0 -7
  236. data/examples/prompts/product_support.md +0 -37
  237. data/examples/prompts/runbook_protocol.md +0 -12
  238. data/examples/prompts/sentiment_analyzer.md +0 -12
  239. data/examples/prompts/skill_a_test.md +0 -4
  240. data/examples/prompts/skill_b_test.md +0 -4
  241. data/examples/prompts/skill_config_test.md +0 -5
  242. data/examples/prompts/skill_cycle_a_test.md +0 -6
  243. data/examples/prompts/skill_cycle_b_test.md +0 -6
  244. data/examples/prompts/skill_description_test.md +0 -4
  245. data/examples/prompts/skill_leaf_test.md +0 -4
  246. data/examples/prompts/skill_nested_test.md +0 -6
  247. data/examples/prompts/skill_refs_main_test.md +0 -6
  248. data/examples/prompts/skill_self_ref_test.md +0 -6
  249. data/examples/prompts/skill_with_mcp_test.md +0 -9
  250. data/examples/prompts/skill_with_params_test.md +0 -6
  251. data/examples/prompts/skill_with_robot_name_test.md +0 -5
  252. data/examples/prompts/skill_with_tools_test.md +0 -6
  253. data/examples/prompts/sre_compliance.md +0 -10
  254. data/examples/prompts/structured_output.md +0 -7
  255. data/examples/prompts/synthesizer.md +0 -17
  256. data/examples/prompts/technical.md +0 -10
  257. data/examples/prompts/template_with_skills_test.md +0 -6
  258. data/examples/prompts/triage.md +0 -22
  259. data/examples/ruboruby.md +0 -423
  260. data/examples/run_all.rb +0 -60
  261. data/examples/xyzzy.rb +0 -97
  262. data/lib/robot_lab/mcp/client.rb +0 -223
  263. data/lib/robot_lab/mcp/connection_poller.rb +0 -195
  264. data/lib/robot_lab/mcp/transports/base.rb +0 -64
  265. data/lib/robot_lab/mcp/transports/sse.rb +0 -120
  266. data/lib/robot_lab/mcp/transports/stdio.rb +0 -170
  267. data/lib/robot_lab/mcp/transports/streamable_http.rb +0 -141
  268. data/lib/robot_lab/mcp/transports/websocket.rb +0 -111
  269. data/logfile +0 -8
  270. data/mkdocs.yml +0 -209
  271. data/robot_concurrency.md +0 -38
  272. data/simple_acp_review.md +0 -298
  273. data/temp.md +0 -6
  274. data/tool_manifest_plan.md +0 -155
@@ -1,396 +0,0 @@
1
- # Robot Execution
2
-
3
- This page details how a robot processes messages and generates responses.
4
-
5
- ## Execution Overview
6
-
7
- When you call `robot.run("message")`, several steps occur:
8
-
9
- ```mermaid
10
- sequenceDiagram
11
- participant App as Application
12
- participant Skills as AgentSkillMatching<br/>(prepended)
13
- participant Robot as Robot (Hooking run)
14
- participant Hooks as Hooks / HookRegistry
15
- participant Memory
16
- participant Ledger as Budget::Ledger
17
- participant Chat as @chat (RubyLLM)
18
- participant LLM
19
-
20
- App->>Skills: run("message")
21
- Skills->>Skills: match_agent_skills + inject (prompt & script tools)
22
- Skills->>Robot: super
23
-
24
- Robot->>Memory: resolve_run_memory()
25
- Robot->>Memory: current_writer = name
26
- Robot->>Hooks: Hooks.run(:run, RunHookContext)
27
- Note over Hooks: before_run / around_run wrap everything below
28
-
29
- Robot->>Robot: prepare_tools() — resolve_mcp_hierarchy,<br/>ensure_mcp_clients, resolve_tools_hierarchy,<br/>filtered_tools + cap_tools
30
- Robot->>Chat: with_tools(*filtered, replace: true)
31
- Robot->>Robot: rerender_template(kwargs) — when template + extra kwargs
32
- Robot->>Ledger: reserve_budget! (raises BudgetExceeded if exhausted)
33
-
34
- Robot->>Hooks: Hooks.run(:llm_generation, LlmGenerationHookContext)
35
- Robot->>Robot: inject_learnings(request)
36
- Robot->>Robot: maybe_compact — fires :compaction family
37
- Robot->>Chat: install_circuit_breaker (when max_tool_rounds)
38
- Robot->>Chat: install_doom_loop_detection (always)
39
- Robot->>Chat: ask(message, &streaming_block)
40
- Chat->>LLM: API Request
41
-
42
- loop Tool loop (inside :llm_generation)
43
- LLM-->>Chat: tool_call response
44
- Chat->>Hooks: Tool.call — Hooks.run(:tool_call)
45
- Chat->>LLM: tool result
46
- end
47
-
48
- LLM-->>Chat: final response
49
- Chat-->>Robot: RubyLLM::Response
50
- Robot->>Robot: build_result(response) — result_text + token accounting
51
- Robot->>Ledger: reconcile_budget!
52
- Robot->>Robot: enforce_token_budget! / enforce_cost_budget!<br/>(raise InferenceError on overage)
53
- Robot->>Hooks: after_run
54
- Note over Robot,Memory: ensure: remove_doom_loop_detection,<br/>restore_tool_call_callback, restore current_writer
55
- Robot-->>Skills: RobotResult
56
- Skills->>Skills: ensure: restore_after_agent_skills
57
- Skills-->>App: RobotResult
58
- ```
59
-
60
- The steps are described individually below. Two things this diagram makes
61
- explicit that the prose repeats: `Robot::AgentSkillMatching` is **prepended**, so
62
- its `run` wraps `Robot::Hooking#run` (which is the `run` everything else calls);
63
- and the `:llm_generation` family wraps the provider's *entire* tool loop, so
64
- `:tool_call` hooks fire nested inside it rather than between generations.
65
-
66
- ## Step-by-Step Flow
67
-
68
- ### 1. Memory Resolution
69
-
70
- `Robot#run` first resolves the memory this run will write to, via `resolve_run_memory`:
71
-
72
- ```ruby
73
- # resolve_active_memory priority order:
74
- # 1. Explicit network_memory: parameter
75
- # 2. Network's memory (if running in a network)
76
- # 3. Robot's inherent @memory (standalone mode)
77
- run_memory = resolve_active_memory(network: network, network_memory: network_memory)
78
-
79
- # A runtime memory: kwarg either replaces (Memory) or merges into (Hash) it
80
- case memory
81
- when Memory then memory
82
- when Hash then run_memory.tap { |m| m.merge!(memory) }
83
- else run_memory
84
- end
85
- ```
86
-
87
- `run` then sets `run_memory.current_writer = @name` for the duration of the run and restores the previous writer in an `ensure` block, so subscription callbacks always see the robot that actually wrote a value.
88
-
89
- The whole run body is wrapped in `RobotLab::Hooks.run(:run, context, ...)` against the registries `[RobotLab.hooks, network&.hooks, @hooks]`.
90
-
91
- ### 2. MCP Hierarchy Resolution
92
-
93
- MCP servers and tools are resolved together in `prepare_tools`, through a hierarchy: **runtime (run/task) > robot build-time > network > global config**.
94
-
95
- ```ruby
96
- # Parent value: task/network RunConfig, then the network object, then global config
97
- parent_value = network_config&.mcp || network_parent_config(network)&.mcp || RobotLab.config.mcp
98
- build_resolved = ToolConfig.resolve_mcp(@mcp_config, parent_value: parent_value)
99
-
100
- # Then resolve the runtime override against the build-time value
101
- resolved_mcp = ToolConfig.resolve_mcp(runtime_mcp, parent_value: build_resolved)
102
- ```
103
-
104
- Values at each level:
105
-
106
- - `:none` -- no MCP servers at this level
107
- - `:inherit` -- use parent level's MCP config
108
- - `Array` -- explicit list of server configurations
109
-
110
- `run`'s default for `mcp:` is `:none`, so a plain `robot.run("...")` resolves to an empty server list and connects nothing, even when `mcp:` was supplied at build time. Pass `mcp: :inherit` to trigger the connection. (`robot.connect_mcp!` connects eagerly against the build-time config, which is useful for reporting connection status at startup.)
111
-
112
- ### 3. MCP Client Initialization
113
-
114
- If MCP servers need to be connected (or reconnected), the robot initializes clients:
115
-
116
- ```ruby
117
- # Connect to each MCP server (ensure_mcp_clients -> init_mcp_client)
118
- client = MCP::Client.new(server_config) # config is POSITIONAL
119
- client.connect
120
-
121
- if client.connected?
122
- @mcp_clients[client.server.name] = client
123
- discover_mcp_tools(client, server_name) # each remote tool becomes a Tool.create wrapper
124
- else
125
- @failed_mcp_configs[server_name] = server_config
126
- end
127
- ```
128
-
129
- Connection failures are logged and recorded, never raised — they surface through `robot.failed_mcp_server_names`. A later run retries only the servers that are still needed and still failed.
130
-
131
- ### 4. Tools Resolution
132
-
133
- Tools are resolved through the same hierarchy, filtered, capped, and applied to the chat:
134
-
135
- ```ruby
136
- # Explicit :none (or a literal []) means "send zero tools this turn"; otherwise
137
- # filter the local + MCP tools by the resolved allowlist and clamp to max_tools.
138
- filtered = explicit_none_tools?(tools) ? [] : cap_tools(filtered_tools(resolved_tools))
139
-
140
- @chat.with_tools(*filtered, replace: true) if filtered.any? || explicit_none_tools?(tools)
141
- ```
142
-
143
- Two details here are load-bearing:
144
-
145
- - **`replace: true`** — RubyLLM's `with_tools` appends by default. On a persistent chat that would let tools accumulate across turns, so the chat is made to hold exactly this turn's resolved set. An explicit `:none` therefore clears the chat's tools to zero.
146
- - **`cap_tools`** — the resolved list is clamped to `RunConfig#max_tools`, which defaults to `DEFAULT_MAX_TOOLS` (128) because most providers reject longer tool arrays. Setting `max_tools` to nil, 0, or a negative number falls back to 128; the cap cannot be disabled. Dropped tools are logged at `warn`.
147
-
148
- Because `run` defaults to `tools: :none`, a plain `robot.run("...")` sends the LLM no tools at all. Pass `tools: :inherit` to send the robot's attached tools; pass an explicit array of names to use it as an allowlist.
149
-
150
- ### 5. LLM Inference
151
-
152
- The message is sent to the LLM via `Agent#ask`, which delegates to `@chat.ask`. `invoke_ask` wraps that call in the `:llm_generation` hook and installs the per-run guards first:
153
-
154
- ```ruby
155
- RobotLab::Hooks.run(:llm_generation, generation_context, registries: ..., per_run_hooks: hooks) do
156
- effective_message = inject_learnings(generation_context.request)
157
- maybe_compact(network: context.network)
158
- install_circuit_breaker if @config.max_tool_rounds
159
- install_doom_loop_detection
160
- ask(effective_message, **kwargs.slice(:with), &effective_streaming_block(block))
161
- end
162
- ```
163
-
164
- Notes on the surrounding behavior:
165
-
166
- - `:llm_generation` fires **exactly once per `robot.run`** — the provider's tool loop happens inside the block, not once per API call.
167
- - Only `:with` from `run`'s keyword arguments reaches `ask`. Every other unrecognized keyword is treated as template re-render context.
168
- - Doom-loop detection is installed unconditionally on every run; `doom_loop_threshold:` (default 3) only tunes it.
169
-
170
- The persistent `@chat` (a `RubyLLM::Chat` instance) handles:
171
-
172
- - Maintaining conversation history
173
- - Sending the system prompt
174
- - Formatting messages for the provider
175
- - Executing the tool call loop automatically
176
-
177
- ### 6. Tool Execution Loop
178
-
179
- RubyLLM's `@chat` handles the tool loop automatically. When the LLM requests a tool call:
180
-
181
- 1. `@chat` identifies the tool from its registered tools
182
- 2. Calls the tool's `execute` method (for `RubyLLM::Tool` subclasses) or `call` method (for `RobotLab::Tool`)
183
- 3. Sends the result back to the LLM
184
- 4. Repeats until the LLM produces a final text response
185
-
186
- The `on_tool_call` and `on_tool_result` callbacks fire during this loop if configured. They are read off the effective `RunConfig` and registered on `@chat` during `Robot#initialize`:
187
-
188
- ```ruby
189
- def register_chat_callbacks
190
- @chat.on_tool_call(&@on_tool_call) if @on_tool_call
191
- @chat.on_tool_result(&@on_tool_result) if @on_tool_result
192
- setup_bus_channel if @bus
193
- end
194
- ```
195
-
196
- When `max_tool_rounds` is set, `install_circuit_breaker` temporarily wraps `on_tool_call` for the duration of the run and raises `ToolLoopError` once the round count is exceeded; the original callback is restored in `run`'s `ensure` block.
197
-
198
- ### 7. Result Construction
199
-
200
- After the LLM responds, `build_result` turns the response into a `RobotResult`:
201
-
202
- ```ruby
203
- def build_result(response, _memory)
204
- text = result_text(response)
205
- output = text ? [TextMessage.new(role: 'assistant', content: text)] : []
206
- # ... reads response.tokens (or input_tokens/output_tokens), accumulates them
207
- # into the robot's running totals, and passes them to RobotResult
208
- end
209
- ```
210
-
211
- `result_text` is deliberately more forgiving than `response.content`. It falls back, in order, to:
212
-
213
- 1. `response.content`, when present and non-empty
214
- 2. `response.thinking` — Ollama routes some models' (e.g. qwen3) entire output through `reasoning_content`, which RubyLLM surfaces as `thinking`
215
- 3. the most recent non-empty assistant message in `@chat.messages`, scoped to messages *after* the last user message, for models that end a turn on a tool call with no trailing text
216
-
217
- Scoping the history fallback to the current turn is what prevents a previous turn's answer from being returned when a thinking-mode model emits nothing in `content`.
218
-
219
- `build_result` also does token accounting: it reads `response.tokens` (falling back to `input_tokens`/`output_tokens`), adds them to the robot's running totals, and stores them on the result. `run` then reconciles the budget reservation and enforces `token_budget` / `cost_budget`, raising **`RobotLab::InferenceError`** when this call's actual usage pushed cumulative usage over budget. (`RobotLab::BudgetExceeded` is the *other* budget error: it is raised by `reserve_budget!` **before** the call, when a prior call already exhausted the dimension. See [Budgets](../guides/observability.md#budgets-token-cost).)
220
-
221
- ## RobotResult
222
-
223
- The result object from a `robot.run` call:
224
-
225
- ```ruby
226
- result = robot.run("Hello!")
227
-
228
- result.robot_name # => "assistant"
229
- result.output # => [TextMessage] synthesized from result_text
230
- result.tool_calls # => [] in practice (see below)
231
- result.stop_reason # => nil — always (see below)
232
- result.created_at # => Time
233
- result.id # => UUID string
234
- result.duration # => Float or nil (elapsed seconds, set in pipeline execution)
235
- result.input_tokens # => Integer
236
- result.output_tokens # => Integer
237
- result.checksum # => "sha256-hex"
238
- result.raw # => raw LLM response object
239
-
240
- # Convenience methods
241
- result.last_text_content # => "Hi there!" (last text message content)
242
- result.reply # => alias for last_text_content
243
- result.has_tool_calls? # => false
244
- result.stopped? # => true
245
- result.export # => Hash (excludes debug fields)
246
- result.to_h / result.to_json
247
- ```
248
-
249
- `stop_reason` is always `nil`. `build_result` assigns it with `response.respond_to?(:stop_reason) ? response.stop_reason : nil`, and `RubyLLM::Message` does not define `stop_reason`, so no `robot.run` result ever reports one. It is dropped from `export` by the `.compact`, and `stopped?` therefore reduces to `!has_tool_calls?`.
250
-
251
- `output` is a single synthesized `TextMessage`, not a transcript of the turn, and `tool_calls` is read off the final assistant message — which no longer carries tool calls once RubyLLM's tool loop has finished, so it is effectively always empty. Observe tool usage through the `on_tool_call`/`on_tool_result` callbacks or the tool hooks instead.
252
-
253
- ## Streaming
254
-
255
- Robots support streaming by passing a block to `run`. The block receives a `RubyLLM::Chunk`; its text is in `content`:
256
-
257
- ```ruby
258
- result = robot.run("Tell me a story") do |chunk|
259
- print chunk.content
260
- end
261
- ```
262
-
263
- `effective_streaming_block` merges the block with any stored `on_content:` callback. If both are present, both fire — the stored `on_content` first, then the runtime block. The merged block is forwarded to `Agent#ask`, which passes it to `@chat.ask`.
264
-
265
- ```ruby
266
- # on_content fires on every run, no block required
267
- robot = RobotLab.build(name: "bot", system_prompt: "...",
268
- on_content: ->(chunk) { print chunk.content })
269
- ```
270
-
271
- `on_content` is read from the robot's own config at construction — a network-level `config:` does not supply it.
272
-
273
- ## Template Resolution
274
-
275
- When a robot has a `template:`, it is resolved during initialization:
276
-
277
- ```ruby
278
- # 1. Parse the template via prompt_manager
279
- parsed = PM.parse(@template)
280
-
281
- # 2. Apply the non-LLM front matter keys: robot_name, description, tools, mcp
282
- # (constructor-provided values win, so these only fill in gaps)
283
- apply_front_matter_extras(parsed.metadata)
284
-
285
- # 3. Turn the LLM keys into a RunConfig. Front matter is the BASE;
286
- # @config (constructor kwargs) merges over it and wins.
287
- effective = RunConfig.from_front_matter(parsed.metadata).merge(@config)
288
- effective.apply_to(@chat, provider: @provider, assume_model_exists: !@provider.nil?)
289
-
290
- # 4. Render the template body with context and set it as system instructions
291
- @chat.with_instructions(parsed.to_s(**resolved_context))
292
- ```
293
-
294
- If required parameters are missing at build time, rendering is skipped and deferred: the first `run` that supplies context calls `rerender_template`, which re-renders and re-appends the inline `system_prompt`. When `skills:` are in play, each skill's body and front matter are accumulated the same way and the bodies are joined before being set as one instructions block.
295
-
296
- ### Front Matter Config Keys
297
-
298
- Templates parse these LLM keys into a `RunConfig`:
299
-
300
- | Key | Effect |
301
- |-----|--------|
302
- | `model` | Sets the LLM model |
303
- | `temperature` | Sets randomness |
304
- | `top_p` | Parsed, but **not applied** — see below |
305
- | `top_k` | Parsed, but **not applied** |
306
- | `max_tokens` | Parsed, but **not applied** |
307
- | `presence_penalty` | Parsed, but **not applied** |
308
- | `frequency_penalty` | Parsed, but **not applied** |
309
- | `stop` | Parsed, but **not applied** |
310
-
311
- `RunConfig#apply_to` dispatches `chat.with_<field>` guarded by `respond_to?`. `RubyLLM::Chat` only defines `with_model` and `with_temperature`, so the other six are silently dropped when they come from front matter. To set them, use constructor kwargs or a `config:` RunConfig — that path goes through `apply_chat_params` → `with_params` and does take effect:
312
-
313
- ```ruby
314
- RobotLab.build(name: "bot", template: :report, max_tokens: 2000, top_p: 0.3)
315
- ```
316
-
317
- Front matter keys that are not LLM fields — `description`, `robot_name`, `tools`, `mcp`, `skills`, `parameters` — all work.
318
-
319
- Template bodies render with ERB: use `<%= var %>`. `{{ var }}` is not interpolated and passes through verbatim.
320
-
321
- ## Model Selection
322
-
323
- The model is determined by:
324
-
325
- 1. Robot's explicit `model:` parameter
326
- 2. Front matter `model` from template
327
- 3. Global `RobotLab.config.ruby_llm.model`
328
-
329
- ```ruby
330
- robot = RobotLab.build(
331
- name: "bot",
332
- model: "claude-sonnet-4" # Takes precedence
333
- )
334
-
335
- # Or configure globally via config files / environment variables
336
- # ROBOT_LAB_RUBY_LLM__MODEL=gpt-4o
337
- ```
338
-
339
- ## SimpleFlow Integration
340
-
341
- When a robot runs inside a network, the `call` method is invoked by SimpleFlow:
342
-
343
- ```mermaid
344
- sequenceDiagram
345
- participant SF as SimpleFlow
346
- participant Task as Task Wrapper
347
- participant Robot
348
- participant Chat as @chat
349
-
350
- SF->>Task: call(result)
351
- Task->>Task: Hooks.run(:task, ...)
352
- Task->>Task: deep_merge(run_params, task_context)
353
- Task->>Robot: call(enhanced_result)
354
- Robot->>Robot: extract_run_context(result)
355
- Robot->>Robot: message = context.delete(:message)
356
- Robot->>Robot: run(message, **context)
357
- Robot->>Chat: ask(message)
358
- Chat-->>Robot: response
359
- Robot-->>SF: result.continue(robot_result)
360
- ```
361
-
362
- The `Task` wrapper runs the `:task` hook family and deep-merges its per-task configuration (context, mcp, tools, memory, config) into `run_params` before delegating to the robot's `call`. Note that a task's `mcp:`/`tools:` land in `run_params` and are therefore consumed by the robot as *runtime* values, not as a separate tier between the network and the robot.
363
-
364
- The base `Robot#call` extracts the message, calls `run`, and records the elapsed time in `RobotResult#duration`. Because the result is stored under `@name.to_sym`, the pipeline context is keyed by the **robot's** name, not the task name. If the robot raises any exception, the error is caught and wrapped in a `RobotResult` so one failing robot does not crash the entire pipeline:
365
-
366
- ```ruby
367
- def call(result)
368
- run_context = extract_run_context(result)
369
- message = run_context.delete(:message)
370
-
371
- start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
372
- robot_result = run(message, **run_context)
373
- robot_result.duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
374
-
375
- result
376
- .with_context(@name.to_sym, robot_result)
377
- .continue(robot_result)
378
- rescue Exception => e
379
- elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
380
- error_result = RobotResult.new(
381
- robot_name: @name,
382
- output: [TextMessage.new(role: 'assistant', content: "Error: #{e.class}: #{e.message}")]
383
- )
384
- error_result.duration = elapsed
385
-
386
- result
387
- .with_context(@name.to_sym, error_result)
388
- .continue(error_result)
389
- end
390
- ```
391
-
392
- ## Next Steps
393
-
394
- - [Network Orchestration](network-orchestration.md) - Multi-robot coordination
395
- - [Core Concepts](core-concepts.md) - Fundamental building blocks
396
- - [Using Tools](../guides/using-tools.md) - Creating and using tools