robot_lab 0.2.6 → 0.2.7

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 (109) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +124 -64
  3. data/docs/api/core/index.md +41 -15
  4. data/docs/api/core/memory.md +247 -29
  5. data/docs/api/core/network.md +255 -33
  6. data/docs/api/core/result.md +120 -32
  7. data/docs/api/core/robot.md +551 -61
  8. data/docs/api/core/state.md +87 -197
  9. data/docs/api/core/tool.md +165 -20
  10. data/docs/api/errors.md +110 -17
  11. data/docs/api/hooks.md +469 -0
  12. data/docs/api/index.md +80 -7
  13. data/docs/api/mcp/client.md +129 -35
  14. data/docs/api/mcp/index.md +164 -23
  15. data/docs/api/mcp/server.md +27 -3
  16. data/docs/api/mcp/transports.md +94 -22
  17. data/docs/api/messages/index.md +26 -3
  18. data/docs/api/messages/text-message.md +33 -11
  19. data/docs/api/messages/tool-call-message.md +27 -4
  20. data/docs/api/messages/tool-result-message.md +23 -4
  21. data/docs/api/messages/user-message.md +45 -8
  22. data/docs/api/skills.md +519 -0
  23. data/docs/api/streaming/context.md +28 -5
  24. data/docs/api/streaming/index.md +57 -11
  25. data/docs/api/support.md +846 -0
  26. data/docs/architecture/core-concepts.md +79 -31
  27. data/docs/architecture/index.md +86 -11
  28. data/docs/architecture/message-flow.md +66 -29
  29. data/docs/architecture/network-orchestration.md +145 -38
  30. data/docs/architecture/robot-execution.md +172 -90
  31. data/docs/architecture/state-management.md +31 -12
  32. data/docs/concepts.md +176 -21
  33. data/docs/examples/basic-chat.md +72 -19
  34. data/docs/examples/index.md +117 -31
  35. data/docs/examples/mcp-server.md +154 -45
  36. data/docs/examples/multi-robot-network.md +91 -21
  37. data/docs/examples/tool-usage.md +104 -37
  38. data/docs/getting-started/configuration.md +284 -97
  39. data/docs/getting-started/installation.md +53 -41
  40. data/docs/getting-started/quick-start.md +51 -6
  41. data/docs/guides/building-robots.md +258 -50
  42. data/docs/guides/creating-networks.md +214 -30
  43. data/docs/guides/hooks.md +141 -54
  44. data/docs/guides/knowledge.md +35 -4
  45. data/docs/guides/mcp-integration.md +211 -44
  46. data/docs/guides/memory.md +103 -12
  47. data/docs/guides/observability.md +95 -47
  48. data/docs/guides/streaming.md +184 -125
  49. data/docs/guides/using-tools.md +237 -17
  50. data/docs/index.md +36 -4
  51. data/examples/01_simple_robot.rb +2 -2
  52. data/examples/02_tools.rb +14 -4
  53. data/examples/03_network.rb +12 -7
  54. data/examples/04_mcp.rb +11 -4
  55. data/examples/05_streaming.rb +8 -5
  56. data/examples/06_prompt_templates.rb +13 -9
  57. data/examples/07_network_memory.rb +5 -5
  58. data/examples/08_llm_config.rb +20 -15
  59. data/examples/09_chaining.rb +4 -4
  60. data/examples/11_network_introspection.rb +4 -4
  61. data/examples/12_message_bus.rb +2 -2
  62. data/examples/13_spawn.rb +2 -2
  63. data/examples/14_rusty_circuit/README.md +1 -0
  64. data/examples/14_rusty_circuit/comic.rb +7 -3
  65. data/examples/14_rusty_circuit/display.rb +14 -0
  66. data/examples/14_rusty_circuit/heckler.rb +8 -6
  67. data/examples/14_rusty_circuit/open_mic.rb +17 -6
  68. data/examples/14_rusty_circuit/scout.rb +17 -10
  69. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +14 -10
  70. data/examples/15_memory_network_and_bus/linux_writer.rb +2 -2
  71. data/examples/15_memory_network_and_bus/os_editor.rb +3 -1
  72. data/examples/15_memory_network_and_bus/os_writer.rb +4 -1
  73. data/examples/16_writers_room/writer.rb +22 -22
  74. data/examples/16_writers_room/writers_room.rb +2 -0
  75. data/examples/17_skills.rb +14 -13
  76. data/examples/18_rails/README.md +20 -1
  77. data/examples/18_rails/app/controllers/chat_controller.rb +5 -1
  78. data/examples/18_rails/app/jobs/robot_run_job.rb +11 -5
  79. data/examples/18_rails/app/robots/chat_robot.rb +11 -0
  80. data/examples/18_rails/config/initializers/robot_lab.rb +8 -0
  81. data/examples/19_token_tracking.rb +25 -9
  82. data/examples/20_circuit_breaker.rb +10 -7
  83. data/examples/21_learning_loop.rb +42 -16
  84. data/examples/22_context_compression.rb +23 -23
  85. data/examples/23_convergence.rb +24 -17
  86. data/examples/24_structured_delegation.rb +13 -8
  87. data/examples/25_history_search.rb +12 -8
  88. data/examples/27_incident_response/incident_response.rb +31 -13
  89. data/examples/28_mcp_discovery.rb +17 -13
  90. data/examples/29_ractor_tools.rb +4 -2
  91. data/examples/30_ractor_network.rb +22 -17
  92. data/examples/31_launch_assessment.rb +20 -9
  93. data/examples/32_newsletter_reader.rb +7 -2
  94. data/examples/33_stock_predictor.rb +34 -13
  95. data/examples/34_agentskills.rb +7 -3
  96. data/examples/35_hooks.rb +18 -8
  97. data/examples/README.md +199 -45
  98. data/examples/common.rb +79 -11
  99. data/examples/xyzzy.rb +8 -1
  100. data/lib/robot_lab/config.rb +10 -5
  101. data/lib/robot_lab/names.rb +402 -0
  102. data/lib/robot_lab/robot/agent_skill_matching.rb +1 -3
  103. data/lib/robot_lab/robot/bus_messaging.rb +16 -8
  104. data/lib/robot_lab/robot/template_rendering.rb +16 -3
  105. data/lib/robot_lab/robot.rb +23 -2
  106. data/lib/robot_lab/version.rb +1 -1
  107. data/lib/robot_lab.rb +21 -15
  108. data/mkdocs.yml +6 -1
  109. metadata +7 -2
data/examples/README.md CHANGED
@@ -6,7 +6,71 @@ Working demonstrations of RobotLab features, from single-robot basics to multi-r
6
6
 
7
7
  - Ruby >= 3.2
8
8
  - `bundle install` (from the project root)
9
- - An LLM API key (e.g., `ANTHROPIC_API_KEY`)
9
+ - [Ollama](https://ollama.com) running locally with the demo model pulled:
10
+
11
+ ```bash
12
+ ollama serve
13
+ ollama pull qwen3.6
14
+ ```
15
+
16
+ Every example runs against a **local** model — no API keys, no egress, no
17
+ per-token cost. The model, provider and endpoint are set in one place,
18
+ `examples/common.rb`:
19
+
20
+ ```ruby
21
+ LLM = {
22
+ default: LlmConfig.new(provider: "ollama", model: "qwen3.6:latest"),
23
+ small: LlmConfig.new(provider: "ollama", model: "qwen2.5:7b"),
24
+ large: LlmConfig.new(provider: "ollama", model: "llama3.3:latest")
25
+ }
26
+ ```
27
+
28
+ Robots pick it up with the `llm_opts` helper:
29
+
30
+ ```ruby
31
+ robot = RobotLab.build(name: "helper", **llm_opts) # default model
32
+ cheap = RobotLab.build(name: "helper", **llm_opts(:small)) # smaller model
33
+ ```
34
+
35
+ `provider:` is not optional here. Ollama models are absent from RubyLLM's
36
+ model registry, so passing `model:` alone raises `RubyLLM::ModelNotFoundError`
37
+ — supplying `provider:` is what makes RubyLLM skip the registry lookup. Note
38
+ also that `RunConfig` has no `provider` field, so provider and model travel
39
+ together on the robot even when other settings come from a shared `RunConfig`.
40
+
41
+ `common.rb` also raises `request_timeout` to 900s. A 20B+ model on consumer
42
+ hardware routinely takes longer than robot_lab's 120s default on a long
43
+ answer, which otherwise surfaces mid-example as `Net::ReadTimeout`. Override
44
+ with `LLM_REQUEST_TIMEOUT`.
45
+
46
+ ## Runtime: pick your model with `LLM_PROFILE`
47
+
48
+ Every example is a *sequence* of LLM calls, so wall-clock is dominated by
49
+ `calls × seconds-per-call`. On `qwen3.6:latest` expect roughly **60-70s per
50
+ call**. That is fine for the single-robot examples and painful for the
51
+ multi-robot ones:
52
+
53
+ | Example | LLM calls | `qwen3.6:latest` | `LLM_PROFILE=small` |
54
+ |---------|-----------|------------------|---------------------|
55
+ | 01, 02, 19, 20 | 1-6 | seconds to ~5 min | fast |
56
+ | 03, 06, 07, 24, 27, 31 | 4-8 | ~5-10 min | ~2 min |
57
+ | 13, 14, 15 | ~20-30 | **30-40 min** | ~5-10 min |
58
+ | 16 | unbounded (600s cap) | usually hits the cap | often completes |
59
+
60
+ `LLM_PROFILE` switches the model for any example without editing it:
61
+
62
+ ```bash
63
+ LLM_PROFILE=small bundle exec ruby examples/14_rusty_circuit/open_mic.rb
64
+ ```
65
+
66
+ Valid values are the keys of `LLM` in `common.rb`: `default`, `small`, `large`.
67
+
68
+ **Long silences are not hangs.** In example 14 the scout writes its notes to
69
+ `output/scout_notes.md` rather than STDOUT, and a scout turn is 1-4 sequential
70
+ calls — minutes of blank terminal. The `· Scout …` lines exist so you can tell
71
+ progress from a stall. Example 16 is the same idea at larger scale: writers
72
+ coordinate through shared memory, and much of the work shows up in
73
+ `output/room.log` rather than on screen.
10
74
 
11
75
  ## Running Examples
12
76
 
@@ -21,17 +85,36 @@ bundle exec rake examples:all
21
85
  bundle exec ruby examples/01_simple_robot.rb
22
86
  ```
23
87
 
88
+ ## Tools require `tools: :inherit` at run time
89
+
90
+ This trips up everyone once. `Robot#run` defaults to `tools: :none`, which
91
+ means *"send zero tools this turn"* — the robot still holds them in
92
+ `local_tools`, but the provider never sees them and the model answers from
93
+ memory instead of calling anything:
94
+
95
+ ```ruby
96
+ robot = RobotLab.build(name: "bot", **llm_opts, local_tools: [Calculator])
97
+
98
+ robot.run("What is 15 * 7?") # Calculator is NEVER offered
99
+ robot.run("What is 15 * 7?", tools: :inherit) # Calculator is offered
100
+ ```
101
+
102
+ The same applies to MCP (`mcp: :inherit`), to network tasks
103
+ (`task :name, robot, tools: :inherit`), and to anything that forwards keywords
104
+ into `run` — including `RobotLab::RailsIntegration::Job`. Examples 02, 04, 06,
105
+ 14, 15, 16, 20, 33 and the Rails app all pass it explicitly.
106
+
24
107
  ## Directory Structure
25
108
 
26
109
  ```
27
110
  examples/
28
- 27_incident_response/ # Phase 5 infra — BusPoller, reactive memory, poller groups
29
- incident_response.rb # Main entrypoint wires up the war room
111
+ common.rb # Shared LLM config (Ollama), llm_opts, output helpers
112
+ xyzzy.rb # Single-file hook extension used by 35
30
113
  01_simple_robot.rb # Basic robot with template
31
114
  02_tools.rb # Robot with custom tools
32
115
  03_network.rb # Multi-robot network with routing
33
116
  04_mcp.rb # MCP server integration (GitHub)
34
- 05_streaming.rb # Real-time streaming events
117
+ 05_streaming.rb # Real-time streaming callbacks
35
118
  06_prompt_templates.rb # Template-based e-commerce support
36
119
  07_network_memory.rb # Shared memory with concurrent robots
37
120
  08_llm_config.rb # Configuration hierarchy demo
@@ -47,6 +130,20 @@ examples/
47
130
  scout.rb # Talent scout with analyst spawning
48
131
  display.rb # Terminal formatting (color, wrapping, file output)
49
132
  prompts/ # Templates for comic, heckler, and scout
133
+ 15_memory_network_and_bus/ # Network + memory + bus + spawn in one pipeline
134
+ editorial_pipeline.rb # Main entrypoint — three writers, editor, chief
135
+ 16_writers_room/ # Self-organizing group: no orchestration at all
136
+ writers_room.rb # Main entrypoint — book and screenplay modes
137
+ 17_skills.rb # Composable skills, flat and recursive
138
+ 18_rails/ # Minimal Rails 8 demo app (full integration)
139
+ app/robots/chat_robot.rb # Robot factory with system prompt + TimeTool
140
+ app/tools/time_tool.rb # Custom RobotLab::Tool subclass
141
+ app/jobs/robot_run_job.rb # Background job with Turbo Stream callbacks
142
+ app/controllers/ # Chat controller (index + create)
143
+ app/views/ # Layout with CDN importmap, chat view with streaming
144
+ app/models/ # RobotLabThread, RobotLabResult
145
+ config/ # Minimal Rails 8 config (async adapters, no asset pipeline)
146
+ db/migrate/ # Migration from generator template
50
147
  19_token_tracking.rb # Per-robot token & cost tracking
51
148
  20_circuit_breaker.rb # Tool loop circuit breaker with max_tool_rounds
52
149
  21_learning_loop.rb # Learning accumulation across runs with robot.learn
@@ -55,19 +152,17 @@ examples/
55
152
  24_structured_delegation.rb # Structured delegation with duration and token tracking
56
153
  25_history_search.rb # Semantic search over a robot's conversation history
57
154
  26_document_store.rb # Embedding-based document store (RAG) via fastembed
155
+ 27_incident_response/ # BusPoller, reactive memory, poller groups
156
+ incident_response.rb # Main entrypoint — wires up the war room
157
+ 28_mcp_discovery.rb # Semantic MCP server selection before connecting
58
158
  29_ractor_tools.rb # Ractor-safe tools: worker pool, freeze_deep, parallel batch
59
159
  30_ractor_network.rb # Ractor network scheduler: dependency waves, parallel_mode
60
160
  31_launch_assessment.rb # 6 parallel analysts, max_concurrent_robots: 4 semaphore cap
61
- 35_hooks.rb # Hook architecture demo using robot_lab-xyzzy
62
- 18_rails/ # Minimal Rails 8 demo app (full integration)
63
- app/robots/chat_robot.rb # Robot factory with system prompt + TimeTool
64
- app/tools/time_tool.rb # Custom RobotLab::Tool subclass
65
- app/jobs/robot_run_job.rb # Background job with Turbo Stream callbacks
66
- app/controllers/ # Chat controller (index + create)
67
- app/views/ # Layout with CDN importmap, chat view with streaming
68
- app/models/ # RobotLabThread, RobotLabResult
69
- config/ # Minimal Rails 8 config (async adapters, no asset pipeline)
70
- db/migrate/ # Migration from generator template
161
+ 32_newsletter_reader.rb # Utility script (no RobotLab) — RSS to Markdown
162
+ 33_stock_generator.rb # Companion publisher for 33_stock_predictor (Redis)
163
+ 33_stock_predictor.rb # Durable cross-session learning via robot_lab-durable
164
+ 34_agentskills.rb # AgentSkills.io folder-format skills, matched per run
165
+ 35_hooks.rb # Hook architecture demo using xyzzy.rb
71
166
  prompts/ # Prompt templates (.md with YAML front matter)
72
167
  ```
73
168
 
@@ -77,43 +172,43 @@ examples/
77
172
 
78
173
  Create and run a basic robot using a prompt template. Sends a single message and displays the response.
79
174
 
80
- **Requires:** LLM API key
175
+ **Requires:** Ollama
81
176
 
82
177
  ### 02 — Tools
83
178
 
84
179
  Give a robot custom tools (`Calculator`, `FortuneCookie`) defined as `RubyLLM::Tool` subclasses. The LLM decides when to call each tool based on the user's request.
85
180
 
86
- **Requires:** LLM API key
181
+ **Requires:** Ollama
87
182
 
88
183
  ### 03 — Multi-Robot Network
89
184
 
90
185
  Build a customer support network with a classifier robot that routes requests to billing, technical, or general specialists. Uses SimpleFlow's optional task activation for conditional routing.
91
186
 
92
- **Requires:** LLM API key
187
+ **Requires:** Ollama
93
188
 
94
189
  ### 04 — MCP Integration
95
190
 
96
191
  Connect to the GitHub MCP server via stdio transport. Part 1 demonstrates direct `MCP::Client` usage (listing tools, calling `search_repositories`). Part 2 wraps the MCP server inside a robot for natural-language queries.
97
192
 
98
- **Requires:** LLM API key, `GITHUB_PERSONAL_ACCESS_TOKEN`, `github-mcp-server` installed
193
+ **Requires:** Ollama, `GITHUB_PERSONAL_ACCESS_TOKEN`, `github-mcp-server` installed
99
194
 
100
195
  ### 05 — Streaming
101
196
 
102
- Real-time streaming of robot responses using `RobotLab::Streaming::Context`. Simulates text deltas with timing to demonstrate the streaming event model, then shows the code pattern for streaming with a robot or network.
197
+ Real-time streaming of robot responses through four routes: the stored `on_content:` callback wired at build time, a per-call block passed to `run()`, both together (stored fires first, then the block), and `on_content` arriving through a `RunConfig`.
103
198
 
104
- **Requires:** None (simulated events, no LLM calls)
199
+ **Requires:** Ollama (makes four live calls)
105
200
 
106
201
  ### 06 — Prompt Templates
107
202
 
108
203
  Full e-commerce support system using prompt_manager templates with YAML front matter. A triage robot classifies customer requests and routes to order, product, or escalation specialists. Demonstrates build-time context (company info, policies) and run-time context (customer data, order history).
109
204
 
110
- **Requires:** LLM API key
205
+ **Requires:** Ollama
111
206
 
112
207
  ### 07 — Network Memory
113
208
 
114
209
  Reactive shared memory with concurrent robots. Multiple analysis robots (sentiment, entity extraction, keywords) run in parallel and write to shared memory. A synthesizer robot waits for all results using blocking reads, then produces a combined analysis. Demonstrates subscriptions, notifications, and network broadcast.
115
210
 
116
- **Requires:** LLM API key
211
+ **Requires:** Ollama
117
212
 
118
213
  ### 08 — LLM Configuration
119
214
 
@@ -155,7 +250,7 @@ Bidirectional robot communication via TypedBus. A comedy critic (Alice) tasks a
155
250
 
156
251
  Demonstrates: Robot subclasses, prompt templates, auto-ack `on_message`, `reply()` convenience, temperature ramping, convergence patterns.
157
252
 
158
- **Requires:** LLM API key
253
+ **Requires:** Ollama
159
254
 
160
255
  ### 13 — Spawning Robots
161
256
 
@@ -163,7 +258,7 @@ Dynamic specialist creation at runtime. A dispatcher robot receives questions, a
163
258
 
164
259
  Demonstrates: `spawn` for dynamic robot creation, lazy bus creation, `on_message` for reply handling, LLM-driven delegation.
165
260
 
166
- **Requires:** LLM API key
261
+ **Requires:** Ollama
167
262
 
168
263
  ### 14 — The Rusty Circuit (Open Mic Night)
169
264
 
@@ -171,27 +266,51 @@ A comedy club where three robots interact through a shared message bus. A comedi
171
266
 
172
267
  Terminal output is color-formatted: comic bits in cyan (left-aligned), heckler reactions in yellow (right-indented), tool annotations dimmed. Scout notes go to `scout_notes.md` instead of STDOUT. The final verdict appears in green on both STDOUT and the scout file.
173
268
 
174
- Demonstrates: Robot subclasses, self-modification via tool side effects, dynamic spawning (`spawn`), shared `:room` channel + personal channels, processing guards for async serialization, `[SILENCE]` opt-out pattern, style reinvention via user-prompt injection.
269
+ Demonstrates: Robot subclasses, self-modification via tool side effects, dynamic spawning (`spawn`), shared `:room` channel + personal channels, `enqueue_delivery` for serializing async deliveries, `[SILENCE]` opt-out pattern, style reinvention via user-prompt injection.
270
+
271
+ **Requires:** Ollama. The most expensive example in the suite — ~30 LLM calls, 30+ minutes on `qwen3.6`. Use `LLM_PROFILE=small` to watch it in under ten.
272
+
273
+ ### 15 — Memory, Network, Bus & Spawn Together
274
+
275
+ An editorial pipeline where three writers advocate for macOS, Windows, and Linux/BSD as a home AI research lab platform. The network runs the writers in parallel and hands their drafts to an editor through shared memory; the Linux writer spawns three distro specialists mid-pipeline; an editor-in-chief outside the pipeline reviews the combined article over the bus and can demand revisions.
276
+
277
+ Demonstrates: all four coordination mechanisms in one program, plus the direct `shared_memory` reference pattern that parallel pipeline steps need (`extract_run_context` deletes from a shared hash, so only the first parallel step would otherwise see `network_memory`).
175
278
 
176
- **Requires:** LLM API key
279
+ **Requires:** Ollama
280
+
281
+ ### 16 — The Writers' Room (Self-Organizing Group)
282
+
283
+ A team of identical writer robots produces a 10-chapter novella with no orchestration, no pipeline, and no assigned roles. Each writer subscribes to a `:room` broadcast channel and a personal channel, and has tools to read/write shared memory, broadcast, DM, spawn more writers, and mark the work complete. The script seeds the room with an assignment and waits.
284
+
285
+ `--screenplay-from output/memory.json` re-runs the room in screenplay mode, adapting a finished book into a 4-act TV movie pilot at scene granularity.
286
+
287
+ Demonstrates: emergent coordination, `clear_messages(keep_system: true)` as a per-message conversation reset, dynamic team growth, memory as the single source of truth.
288
+
289
+ **Requires:** Ollama (long-running — `--timeout` defaults to 600s)
290
+
291
+ ### 17 — Composable Skills
292
+
293
+ Skills are ordinary templates whose bodies are prepended to the main template. An SRE incident responder is composed from `runbook_protocol` and `structured_output` (flat skills) plus `sre_compliance`, which recursively expands to `pii_redactor` and `audit_trail`. The assembled system prompt is printed line-by-line so the depth-first expansion order is visible, and a skill-less robot is built alongside it for size comparison.
294
+
295
+ **Requires:** Ollama
177
296
 
178
297
  ### 19 — Token & Cost Tracking
179
298
 
180
- Track token usage across runs using `result.input_tokens` / `result.output_tokens` for per-run counts and `robot.total_input_tokens` / `robot.total_output_tokens` for running totals. Demonstrates `reset_token_totals` to start a fresh batch and includes a simple cost estimate using per-provider pricing constants.
299
+ Track token usage across runs using `result.input_tokens` / `result.output_tokens` for per-run counts and `robot.total_input_tokens` / `robot.total_output_tokens` for running totals. Demonstrates `reset_token_totals` to start a fresh batch. Local inference is free, so cost is reported as `$0.00000 (local)`; set `RATE_INPUT_CPM` / `RATE_OUTPUT_CPM` to price the same traffic against a hosted provider.
181
300
 
182
- **Requires:** LLM API key
301
+ **Requires:** Ollama
183
302
 
184
303
  ### 20 — Tool Loop Circuit Breaker
185
304
 
186
305
  Guards against runaway tool call loops using `max_tool_rounds:`. A step processor tool is designed to always return "more steps remain", which would loop indefinitely without a guard. The circuit breaker fires after the configured limit and raises `RobotLab::ToolLoopError`. Shows how to rescue the error gracefully and confirms the robot is fully reusable after a breaker trip.
187
306
 
188
- **Requires:** LLM API key
307
+ **Requires:** Ollama
189
308
 
190
309
  ### 21 — Learning Accumulation Loop
191
310
 
192
311
  Builds up cross-run observations with `robot.learn(text)`. A code reviewer accumulates one key insight after each review. On subsequent runs, learnings are automatically prepended to the user message as a "LEARNINGS FROM PREVIOUS RUNS:" block. Demonstrates bidirectional substring deduplication (broader learnings replace narrower ones), the `robot.learnings` accessor, and how learnings survive a robot rebuild via the shared `Memory` object.
193
312
 
194
- **Requires:** LLM API key
313
+ **Requires:** Ollama
195
314
 
196
315
  ### 22 — Context Window Compression
197
316
 
@@ -199,23 +318,17 @@ Demonstrates `robot.compress_history()` for reducing token usage in long convers
199
318
 
200
319
  **Requires:** `gem 'classifier', '~> 2.3'` in your Gemfile (no LLM calls in the demo itself)
201
320
 
202
- ### 24 — Structured Delegation
203
-
204
- A manager robot delegates sub-tasks to a summarizer and an analyst. Each `delegate()` call returns a `RobotResult` annotated with `delegated_by`, `duration`, and token counts. Includes a comparison table of when to use delegation vs. bus messaging vs. pipelines.
205
-
206
- **Requires:** LLM API key
207
-
208
321
  ### 23 — Debate Convergence Detection
209
322
 
210
- Demonstrates `RobotLab::Convergence` for detecting when two independent agents have reached the same conclusion. Scores pairs of texts from identical → semantically similar → partially related → unrelated, showing how the similarity metric varies. Includes the router fast-path pattern: when two verifier robots agree above a threshold, the expensive reconciler LLM call is skipped entirely.
323
+ Demonstrates `RobotLab::Convergence` for detecting when two independent agents have reached the same conclusion. Scores pairs of texts from identical → semantically similar → partially related → unrelated, showing how the similarity metric varies. Includes the fast-path pattern: a gate robot compares two verifiers' replies and only activates the expensive reconciler via SimpleFlow optional-task activation — when they disagree.
211
324
 
212
325
  **Requires:** `gem 'classifier', '~> 2.3'` in your Gemfile (no LLM calls in the demo itself)
213
326
 
214
327
  ### 24 — Structured Delegation
215
328
 
216
- Demonstrates `robot.delegate(to:, task:)` for synchronous and asynchronous inter-robot delegation. The manager robot delegates document analysis to a summarizer and an analyst. Shows synchronous (sequential, blocking) and asynchronous (parallel fan-out, `DelegationFuture`) modes with wall-time comparison.
329
+ Demonstrates `robot.delegate(to:, task:)` for synchronous and asynchronous inter-robot delegation. The manager robot delegates document analysis to a summarizer and an analyst. Shows synchronous (sequential, blocking) and asynchronous (parallel fan-out, `DelegationFuture`) modes with wall-time comparison. Each result carries `delegated_by`, `duration`, and token counts.
217
330
 
218
- **Requires:** LLM API key
331
+ **Requires:** Ollama
219
332
 
220
333
  ### 25 — Chat History Search
221
334
 
@@ -249,7 +362,7 @@ bundle exec ruby examples/27_incident_response/incident_response.rb
249
362
  bundle exec rake examples:run[27]
250
363
  ```
251
364
 
252
- **Requires:** LLM API key
365
+ **Requires:** Ollama
253
366
 
254
367
  ### 28 — MCP Server Discovery
255
368
 
@@ -297,7 +410,7 @@ and the `pipeline.step_dependencies` dependency graph inspection.
297
410
 
298
411
  **Part 3** — Live LLM run (enabled automatically when `ANTHROPIC_API_KEY` is set).
299
412
 
300
- **Requires:** None for Parts 1 & 2. LLM API key for Part 3.
413
+ **Requires:** None for Parts 1 & 2. `RUN_LIVE=1` plus Ollama for Part 3 (expected to fail — ruby_llm is not Ractor-safe yet).
301
414
 
302
415
  ### 31 — Product Launch Assessment (Concurrency Cap)
303
416
 
@@ -305,13 +418,37 @@ Six specialist robots evaluate a product launch simultaneously: market, competit
305
418
 
306
419
  Demonstrates: `max_concurrent_robots:` on `RunConfig`, `Async::Semaphore` back-pressure via `simple_flow`, six parallel `depends_on: :none` tasks, shared memory writes and blocking reads.
307
420
 
308
- **Requires:** LLM API key
421
+ **Requires:** Ollama
422
+
423
+ ### 32 — Newsletter Reader
424
+
425
+ A plain utility script that fetches unprocessed issues from Ruby newsletter RSS feeds and saves them as Markdown. **It uses no part of RobotLab** — it lives here as a content feeder for other experiments, not as a capability demo. Set `CLIPPINGS_DIR` to choose the output folder.
426
+
427
+ **Requires:** `html2markdown` on `PATH`
428
+
429
+ ### 33 — Durable Cross-Session Learning
430
+
431
+ `33_stock_generator.rb` publishes synthetic XYZZY prices to a Redis channel using geometric Brownian motion. `33_stock_predictor.rb` consumes them, predicts each window's high/low with an SMA + EMA ensemble, and after every window asks a tuner robot to adjust the predictor's parameters.
432
+
433
+ The tuner uses `robot_lab-durable`. Note the API: `learn:` / `learn_domain:` are **not** constructor parameters (`Robot#initialize` takes a fixed keyword list with no `**rest`, so passing them raises `ArgumentError`). Call `robot.setup_durable_learning(domain:)` after `build`, which seeds `robot.learnings` from `~/.robot_lab/durable/<domain>.yml` and appends the `RecallKnowledge` / `RecordKnowledge` tools. Core `Robot#run` does not invoke the reflector, so the caller drives `robot.run_reflector` to promote learnings back to the store.
434
+
435
+ Run the two scripts in separate terminals.
436
+
437
+ **Requires:** Ollama, Redis on localhost:6379, `robot_lab-durable`
438
+
439
+ ### 34 — AgentSkills.io Integration
440
+
441
+ Skills declared as `skills: [:code_reviewer]` are resolved from `~/.prompts/skills/<name>/SKILL.md` and matched per-run by embedding similarity — a code-review question activates the skill, an unrelated question does not. Run the two queries and compare.
442
+
443
+ **Requires:** Ollama, `robot_lab-document_store` gem, a `SKILL.md` at `~/.prompts/skills/code_reviewer/`
309
444
 
310
445
  ### 35 — Hooks Architecture
311
446
 
312
- Loads the local `robot_lab-xyzzy` extension, which registers for every hook and logs each callback with the context it receives. Demonstrates robot run, LLM generation, tool call, network run, task, and error hooks with deterministic stubbed responses.
447
+ Loads `xyzzy.rb`, a single-file `RobotLab::Hook` subclass that registers for every hook and logs each callback with the context it receives. Demonstrates robot run, LLM generation, tool call, network run, task, and error hooks. Stubs `robot.chat.ask` for the deterministic sections; the LLM-generation section makes four real calls to show `around_llm_generation` serving two of them from a cache.
313
448
 
314
- **Requires:** None (no LLM calls)
449
+ `RobotLab::Hook` has no logger accessor — handlers own their output. `xyzzy.rb` freezes its log destination from `XYZZY_LOG_PATH` at load time, so the env var must be set before the `require`.
450
+
451
+ **Requires:** Ollama (four calls in the LLM-loop section)
315
452
 
316
453
  ### 18 — Rails Integration Demo
317
454
 
@@ -326,13 +463,15 @@ A minimal, hand-built Rails 8 app that exercises every piece of RobotLab's Rails
326
463
 
327
464
  **No Redis, no Solid Queue, no asset pipeline.** Uses `:async` adapters for both ActiveJob and ActionCable. Turbo JS loaded via importmap from CDN (`@hotwired/turbo-rails`).
328
465
 
466
+ **Two API details worth copying:** the job's superclass is `RobotLab::RailsIntegration::Job` (there is no `RobotLab::Job` alias), and the controller enqueues with `tools: :inherit` so that keyword reaches `robot.run` — otherwise `TimeTool` is never offered and the model guesses the time.
467
+
329
468
  ```bash
330
469
  cd examples/18_rails
331
470
  bin/setup # bundle install + db:create + db:migrate
332
471
  bin/dev # starts Puma on http://localhost:3000
333
472
  ```
334
473
 
335
- **Requires:** LLM API key, Ruby 3.2+
474
+ **Requires:** Ollama, Ruby 3.2+
336
475
 
337
476
  ## Prompt Templates
338
477
 
@@ -377,3 +516,18 @@ Front matter keys like `model`, `temperature`, `top_p`, `max_tokens` are applied
377
516
  | `open_mic_comic.md` | 14 | Observational comedian with self-modification |
378
517
  | `open_mic_heckler.md` | 14 | Tough audience heckler (can stay silent or counter-joke) |
379
518
  | `open_mic_scout.md` | 14 | Talent scout with analyst recruitment |
519
+ | `os_advocate.md` | 15 | Operating-system advocacy writer |
520
+ | `os_editor.md` | 15 | Synthesizes the three advocacy drafts |
521
+ | `os_chief.md` | 15 | Editor-in-chief (APPROVED / REVISE) |
522
+ | `writer.md` | 16 | Self-organizing novella writer |
523
+ | `screenplay_writer.md` | 16 | Self-organizing screenplay writer |
524
+ | `incident_responder.md` | 17 | SRE on-call analyst (main template) |
525
+ | `runbook_protocol.md` | 17 | Flat skill: 5-step incident protocol |
526
+ | `structured_output.md` | 17 | Flat skill: JSON response format |
527
+ | `sre_compliance.md` | 17 | Recursive skill: bundles the two leaves below |
528
+ | `pii_redactor.md` | 17 | Leaf skill: redact PII |
529
+ | `audit_trail.md` | 17 | Leaf skill: audit metadata |
530
+
531
+ Templates matching `*_test.md` are fixtures for the test suite, not examples.
532
+
533
+ Templates for 14, 15 and 16 live in those examples' own `prompts/` directories; each entrypoint sets `ROBOT_LAB_TEMPLATE_PATH` before requiring `common.rb`.
data/examples/common.rb CHANGED
@@ -7,27 +7,95 @@ ENV["ROBOT_LAB_TEMPLATE_PATH"] ||= File.join(__dir__, "prompts")
7
7
 
8
8
  require_relative "../lib/robot_lab"
9
9
 
10
+ # ── Local LLM Configuration ───────────────────────────────────────────────────
11
+ #
12
+ # Every example runs against a LOCAL model served by Ollama. No API keys, no
13
+ # network egress, no per-token cost. Pull the model once before running:
14
+ #
15
+ # ollama pull qwen3.6
16
+ #
17
+ # Ollama models are not in RubyLLM's model registry, so a `provider:` must be
18
+ # supplied alongside `model:` — that is what makes RubyLLM skip the registry
19
+ # lookup (see Robot#initialize, which sets assume_model_exists when provider is
20
+ # given). Use the `llm_opts` helper below so every robot gets both.
21
+
10
22
  LlmConfig = Data.define(:provider, :model)
11
23
 
12
24
  LLM = {
13
- default: LlmConfig.new(provider: "openai", model: "gpt-4.1-mini"),
14
- local: LlmConfig.new(provider: "ollama", model: "llama3.2"),
15
- anthropic: LlmConfig.new(provider: "anthropic", model: "claude-opus-4-7")
25
+ default: LlmConfig.new(provider: "ollama", model: "qwen3.6:latest"),
26
+ small: LlmConfig.new(provider: "ollama", model: "qwen2.5:7b"),
27
+ large: LlmConfig.new(provider: "ollama", model: "llama3.3:latest")
16
28
  }.freeze
17
29
 
18
- RubyLLM.configure do |c|
19
- c.logger = Logger.new(File::NULL)
20
- c.default_model = LLM[:default].model
21
- c.openai_api_key = ENV['OPENAI_API_KEY']
22
- c.openai_organization_id = ENV['OPENAI_ORGANIZATION_ID']
23
- c.openai_project_id = ENV['OPENAI_PROJECT_ID']
24
- c.anthropic_api_key = ENV['ANTHROPIC_API_KEY']
25
- end
30
+ OLLAMA_API_BASE = ENV.fetch("OLLAMA_API_BASE", "http://localhost:11434/v1")
26
31
 
32
+ # ORDER MATTERS. The first touch of RobotLab.config runs Config#after_load,
33
+ # which calls RubyLLM.configure itself and would clobber anything set before
34
+ # it. So configure RobotLab first, then RubyLLM — the later block wins.
27
35
  RobotLab.configure do |c|
28
36
  c.logger = Logger.new(File::NULL)
29
37
  end
30
38
 
39
+ RubyLLM.configure do |c|
40
+ c.logger = Logger.new(File::NULL)
41
+ c.default_model = LLM[:default].model
42
+ c.ollama_api_base = OLLAMA_API_BASE
43
+
44
+ # A large local model on consumer hardware is far slower than a hosted API,
45
+ # and robot_lab's bundled 120s default is comfortably exceeded by a long
46
+ # answer from a 20B+ model — which surfaces mid-run as Net::ReadTimeout.
47
+ #
48
+ # Integer(), not the raw env string: this value reaches Net::HTTP directly,
49
+ # and a String raises "can't convert String into time interval". That is
50
+ # also why the ROBOT_LAB_RUBY_LLM__REQUEST_TIMEOUT env var is the wrong
51
+ # lever here — env values arrive as strings.
52
+ c.request_timeout = Integer(ENV.fetch("LLM_REQUEST_TIMEOUT", "900"))
53
+ c.max_retries = 1
54
+ end
55
+
56
+ # Which LLM entry an unqualified llm_opts resolves to. Lets you run any
57
+ # example against a faster model without editing it — worth knowing for the
58
+ # multi-robot demos (14, 15, 16), which are 30+ sequential LLM calls and take
59
+ # over half an hour on a 20B+ model:
60
+ #
61
+ # LLM_PROFILE=small bundle exec ruby examples/14_rusty_circuit/open_mic.rb
62
+ LLM_PROFILE = ENV.fetch("LLM_PROFILE", "default").to_sym
63
+
64
+ unless LLM.key?(LLM_PROFILE)
65
+ abort "Unknown LLM_PROFILE #{LLM_PROFILE.inspect}. Choose one of: #{LLM.keys.join(', ')}"
66
+ end
67
+
68
+ # Provider + model keyword pair for RobotLab.build / Robot.new.
69
+ #
70
+ # Both are required for a local Ollama model. Splat it into any robot
71
+ # constructor:
72
+ #
73
+ # RobotLab.build(name: "helper", **llm_opts) # honors LLM_PROFILE
74
+ # RobotLab.build(name: "cheap", **llm_opts(:small)) # pinned regardless
75
+ #
76
+ # @param key [Symbol, nil] which entry of LLM to use; defaults to LLM_PROFILE
77
+ # @return [Hash] { provider:, model: }
78
+ def llm_opts(key = nil)
79
+ cfg = LLM.fetch(key || LLM_PROFILE)
80
+ { provider: cfg.provider, model: cfg.model }
81
+ end
82
+
83
+ # Fail fast with an actionable message when Ollama isn't reachable, instead of
84
+ # letting every example die inside an HTTP adapter.
85
+ def require_ollama!
86
+ require "net/http"
87
+ uri = URI(OLLAMA_API_BASE.sub(%r{/v1/?$}, "") + "/api/tags")
88
+ Net::HTTP.start(uri.host, uri.port, open_timeout: 2, read_timeout: 2) { |h| h.get(uri.request_uri) }
89
+ rescue StandardError => e
90
+ abort <<~ERROR
91
+ Cannot reach Ollama at #{OLLAMA_API_BASE} (#{e.class}).
92
+
93
+ Start it and pull the model used by the examples:
94
+ ollama serve
95
+ ollama pull #{LLM[:default].model.sub(/:latest\z/, "")}
96
+ ERROR
97
+ end
98
+
31
99
  # ── Example Output Helpers ─────────────────────────────────────────────────────
32
100
 
33
101
  module ExOut
data/examples/xyzzy.rb CHANGED
@@ -13,7 +13,12 @@
13
13
  # stdout : one tagline per hook call → [xyzzy] HH:MM:SS.mmm hook_name
14
14
  # logfile: full context snapshot written to LOG_PATH via PP.pp
15
15
  #
16
+ # The log destination is read from XYZZY_LOG_PATH at load time and frozen
17
+ # into a constant. Set it BEFORE requiring this file to redirect the log;
18
+ # there is deliberately no writer method, so the constant stays shareable.
19
+ #
16
20
  # Usage (from any example that requires common):
21
+ # ENV["XYZZY_LOG_PATH"] = "/tmp/xyzzy.log" # optional, must precede require
17
22
  # require_relative "xyzzy"
18
23
 
19
24
  require "pp"
@@ -23,7 +28,9 @@ module RobotLab
23
28
  class Xyzzy < Hook
24
29
  self.namespace = :xyzzy
25
30
 
26
- LOG_PATH = File.expand_path("~/.robot_lab/xyzzy_hooks.log").freeze
31
+ LOG_PATH = File.expand_path(
32
+ ENV.fetch("XYZZY_LOG_PATH", "~/.robot_lab/xyzzy_hooks.log")
33
+ ).freeze
27
34
  FileUtils.mkdir_p(File.dirname(LOG_PATH))
28
35
 
29
36
  class << self
@@ -8,7 +8,8 @@ module RobotLab
8
8
  # Provides:
9
9
  # - Nested configuration with a dedicated `ruby_llm:` section
10
10
  # - Environment-specific settings (development, test, production)
11
- # - XDG config file loading (~/.config/robot_lab/config.yml)
11
+ # - XDG config file loading (~/.config/robot_lab/robot_lab.yml — the filename
12
+ # repeats the `config_name`; `config.yml` is never read)
12
13
  # - Environment variable overrides (ROBOT_LAB_*)
13
14
  # - Automatic RubyLLM configuration application
14
15
  #
@@ -21,10 +22,14 @@ module RobotLab
21
22
  # # ROBOT_LAB_RUBY_LLM__MODEL=gpt-4
22
23
  # # ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...
23
24
  #
24
- # @example User config file (~/.config/robot_lab/config.yml)
25
- # defaults:
26
- # ruby_llm:
27
- # anthropic_api_key: <%= ENV['ANTHROPIC_API_KEY'] %>
25
+ # @example User config file (~/.config/robot_lab/robot_lab.yml)
26
+ # # Flat keys, or a section named for the current environment. A `defaults:`
27
+ # # wrapper is IGNORED here — it applies only to the gem's bundled
28
+ # # defaults.yml. This file is NOT run through ERB, so keep secrets in
29
+ # # environment variables or in ./config/robot_lab.yml (which is).
30
+ # ruby_llm:
31
+ # model: claude-sonnet-4
32
+ # request_timeout: 120
28
33
  #
29
34
  class Config < MywayConfig::Base
30
35
  config_name :robot_lab