robot_lab 0.3.0 → 0.3.1

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