robot_lab 0.0.7 → 0.0.9

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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +99 -3
  3. data/README.md +125 -26
  4. data/Rakefile +39 -1
  5. data/docs/api/core/robot.md +284 -8
  6. data/docs/api/core/tool.md +28 -2
  7. data/docs/api/mcp/client.md +1 -0
  8. data/docs/api/mcp/server.md +27 -8
  9. data/docs/api/mcp/transports.md +21 -6
  10. data/docs/architecture/core-concepts.md +1 -1
  11. data/docs/architecture/robot-execution.md +20 -2
  12. data/docs/concepts.md +11 -3
  13. data/docs/examples/index.md +1 -1
  14. data/docs/examples/rails-application.md +1 -1
  15. data/docs/guides/building-robots.md +245 -11
  16. data/docs/guides/creating-networks.md +18 -0
  17. data/docs/guides/mcp-integration.md +74 -2
  18. data/docs/guides/rails-integration.md +145 -17
  19. data/docs/guides/using-tools.md +45 -4
  20. data/docs/index.md +62 -6
  21. data/examples/05_streaming.rb +113 -94
  22. data/examples/14_rusty_circuit/.gitignore +1 -0
  23. data/examples/14_rusty_circuit/open_mic.rb +1 -1
  24. data/examples/17_skills.rb +242 -0
  25. data/examples/18_rails/.envrc +3 -0
  26. data/examples/18_rails/.gitignore +5 -0
  27. data/examples/18_rails/Gemfile +10 -0
  28. data/examples/18_rails/README.md +48 -0
  29. data/examples/18_rails/Rakefile +4 -0
  30. data/examples/18_rails/app/controllers/application_controller.rb +4 -0
  31. data/examples/18_rails/app/controllers/chat_controller.rb +46 -0
  32. data/examples/18_rails/app/jobs/application_job.rb +4 -0
  33. data/examples/18_rails/app/jobs/robot_run_job.rb +79 -0
  34. data/examples/18_rails/app/models/application_record.rb +5 -0
  35. data/examples/18_rails/app/models/robot_lab_result.rb +36 -0
  36. data/examples/18_rails/app/models/robot_lab_thread.rb +23 -0
  37. data/examples/18_rails/app/robots/chat_robot.rb +14 -0
  38. data/examples/18_rails/app/tools/time_tool.rb +9 -0
  39. data/examples/18_rails/app/views/chat/_user_message.html.erb +1 -0
  40. data/examples/18_rails/app/views/chat/index.html.erb +67 -0
  41. data/examples/18_rails/app/views/layouts/application.html.erb +49 -0
  42. data/examples/18_rails/bin/dev +7 -0
  43. data/examples/18_rails/bin/rails +6 -0
  44. data/examples/18_rails/bin/setup +15 -0
  45. data/examples/18_rails/config/application.rb +33 -0
  46. data/examples/18_rails/config/cable.yml +2 -0
  47. data/examples/18_rails/config/database.yml +5 -0
  48. data/examples/18_rails/config/environment.rb +4 -0
  49. data/examples/18_rails/config/initializers/robot_lab.rb +3 -0
  50. data/examples/18_rails/config/routes.rb +6 -0
  51. data/examples/18_rails/config.ru +4 -0
  52. data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +32 -0
  53. data/examples/README.md +30 -0
  54. data/examples/prompts/audit_trail.md +8 -0
  55. data/examples/prompts/incident_responder.md +18 -0
  56. data/examples/prompts/parameterized_main_test.md +6 -0
  57. data/examples/prompts/pii_redactor.md +7 -0
  58. data/examples/prompts/runbook_protocol.md +12 -0
  59. data/examples/prompts/skill_a_test.md +4 -0
  60. data/examples/prompts/skill_b_test.md +4 -0
  61. data/examples/prompts/skill_config_test.md +5 -0
  62. data/examples/prompts/skill_cycle_a_test.md +6 -0
  63. data/examples/prompts/skill_cycle_b_test.md +6 -0
  64. data/examples/prompts/skill_description_test.md +4 -0
  65. data/examples/prompts/skill_leaf_test.md +4 -0
  66. data/examples/prompts/skill_nested_test.md +6 -0
  67. data/examples/prompts/skill_refs_main_test.md +6 -0
  68. data/examples/prompts/skill_self_ref_test.md +6 -0
  69. data/examples/prompts/skill_with_params_test.md +6 -0
  70. data/examples/prompts/sre_compliance.md +10 -0
  71. data/examples/prompts/structured_output.md +7 -0
  72. data/examples/prompts/template_with_skills_test.md +6 -0
  73. data/lib/generators/robot_lab/install_generator.rb +12 -0
  74. data/lib/generators/robot_lab/templates/initializer.rb.tt +36 -22
  75. data/lib/generators/robot_lab/templates/job.rb.tt +92 -0
  76. data/lib/generators/robot_lab/templates/robot.rb.tt +6 -21
  77. data/lib/generators/robot_lab/templates/robot_test.rb.tt +5 -3
  78. data/lib/generators/robot_lab/templates/routing_robot.rb.tt +41 -35
  79. data/lib/robot_lab/mcp/client.rb +6 -4
  80. data/lib/robot_lab/mcp/server.rb +21 -3
  81. data/lib/robot_lab/mcp/transports/base.rb +10 -2
  82. data/lib/robot_lab/mcp/transports/stdio.rb +52 -26
  83. data/lib/robot_lab/{rails → rails_integration}/engine.rb +1 -1
  84. data/lib/robot_lab/{rails → rails_integration}/railtie.rb +1 -1
  85. data/lib/robot_lab/rails_integration/turbo_stream_callbacks.rb +72 -0
  86. data/lib/robot_lab/robot/mcp_management.rb +61 -4
  87. data/lib/robot_lab/robot/template_rendering.rb +181 -4
  88. data/lib/robot_lab/robot.rb +196 -33
  89. data/lib/robot_lab/robot_result.rb +3 -1
  90. data/lib/robot_lab/run_config.rb +1 -1
  91. data/lib/robot_lab/tool.rb +26 -0
  92. data/lib/robot_lab/version.rb +1 -1
  93. data/lib/robot_lab.rb +6 -4
  94. metadata +55 -19
  95. data/examples/14_rusty_circuit/scout_notes.md +0 -89
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1eaa090a87b2f82890dc8ee350bb1326b380c41d6560c3ae01e0ea1a4fc0d84a
4
- data.tar.gz: 8232818730ba698d09ed9537adbc774f5d2ad84013942016e48ebad192a653c9
3
+ metadata.gz: c852fcf7f4aed4ce95fabdc5b0296723ca8aa10e780dabaa7759e618a22bc640
4
+ data.tar.gz: 1bcb205c958ede9967886dae78a1d1a6d47da42e4cd9bd29d7bdd3e094b0a088
5
5
  SHA512:
6
- metadata.gz: ce75a3733e3d153196f85995c1363e542aa9b039f523cb15388a2d0d0f7a13d7ea26390a12abf6582680da5b119f1df8f6f35a7837a28a8b6f07f5aca72639db
7
- data.tar.gz: c2c4f50e6352dc6018c8f0077549450e05adccb3eaa555236c46b131c557f69acf6f59ca6af12c823e429db777019bca684846338af273de2e2ce5587be5a976
6
+ metadata.gz: 5620e7798ac04441cb23c6a7cc5f0cdad7447103825db35ef6f3a3987785b8ff5fb355ec03a309ef9c8a5ce5b0b7a29d9f5adef0e6a5d9de5cd66d3c94fb0469
7
+ data.tar.gz: 9300b1f5ed98e70226c7c670bcf2e3dee033310db6b2182b2705085f02474a1ea6157a011c93906da1d45ba38b4c9f8b9e62545cdb5fd304ca1550734f7dc043
data/CHANGELOG.md CHANGED
@@ -1,8 +1,5 @@
1
1
  # Changelog
2
2
 
3
- > [!CAUTION]
4
- > This gem is under active development. APIs and features may change without notice.
5
-
6
3
  All notable changes to this project will be documented in this file.
7
4
 
8
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
@@ -11,6 +8,105 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
8
 
12
9
  ## [Unreleased]
13
10
 
11
+ ## [0.0.9] - 2026-03-02
12
+
13
+ ### Added
14
+
15
+ - **Provider passthrough** — `provider:` parameter on Robot constructor for local LLM providers (Ollama, GPUStack, etc.)
16
+ - Automatically sets `assume_model_exists: true` when provider is specified
17
+ - Exposed via `robot.provider` accessor
18
+ - **MCP request timeouts** — configurable timeout for all MCP transports
19
+ - `MCP::Server` accepts `timeout:` parameter (default 15s); auto-converts millisecond values
20
+ - `MCP::Transports::Base` extracts and exposes `timeout` from config
21
+ - `MCP::Transports::Stdio` wraps all blocking I/O with `Timeout.timeout` — hung servers no longer block the caller forever
22
+ - Timeout propagated from `MCP::Server` through `MCP::Client` to transport layer
23
+ - **MCP connection resilience** — improved error handling and retry logic
24
+ - `ensure_mcp_clients` retries previously failed servers on subsequent calls
25
+ - `@failed_mcp_configs` tracks servers that failed to connect
26
+ - `robot.failed_mcp_server_names` — query which MCP servers are down
27
+ - `robot.connect_mcp!` — eagerly connect to MCP servers (normally lazy)
28
+ - `init_mcp_client` rescues `StandardError` so one bad server doesn't prevent others from connecting
29
+ - `cleanup_process` in Stdio transport for reliable resource cleanup
30
+ - Better error messages for command-not-found (`Errno::ENOENT`), broken pipe (`Errno::EPIPE`), and EOF conditions
31
+ - **`robot.inject_mcp!`** — inject pre-connected MCP clients and tools from an external host application
32
+ - **Conversation management APIs** on Robot
33
+ - `robot.chat` — access the underlying `RubyLLM::Chat` instance
34
+ - `robot.messages` — return conversation messages
35
+ - `robot.clear_messages(keep_system:)` — clear history, optionally preserving the system prompt
36
+ - `robot.replace_messages(messages)` — restore a saved conversation (checkpoint/restore)
37
+ - `robot.chat_provider` — query the provider name without reaching into chat internals
38
+ - `robot.mcp_client(server_name)` — find an MCP client by server name
39
+ - **`RobotResult#duration`** — elapsed seconds for a robot run, set automatically during pipeline execution
40
+ - **`RobotResult#raw`** — raw LLM response stored on every result (previously only settable via accessor)
41
+ - **Pipeline error resilience** — `Robot#call` (pipeline step) rescues all exceptions so one failing robot doesn't crash the entire network; error is captured in a `RobotResult` with the elapsed duration
42
+
43
+ ### Changed
44
+
45
+ - Bumped version to 0.0.9
46
+ - Display `scout_path` in Rusty Circuit example updated to use `output/` subdirectory
47
+ - Updated `onnxruntime` dependency to 0.11.0
48
+ - Updated Gemfile.lock dependencies (erb, minitest, rails-html-sanitizer, json_schemer)
49
+
50
+ ## [0.0.8] - 2026-02-22
51
+
52
+ ### Added
53
+
54
+ - **Skills as composable templates** — prepend reusable prompt snippets at build time via `skills:` parameter
55
+ - Skills are regular PromptManager templates (no special subdirectory)
56
+ - Recursive expansion — skills can declare nested skills via front matter `skills:` key
57
+ - Depth-first ordering — nested skills appear before their parent
58
+ - Cycle detection via `Set` of visited IDs; cycles log a warning and skip
59
+ - Config cascade — skill₁ → skill₂ → ... → main template → constructor kwargs
60
+ - Shared context — all skills and the main template render with the same `context:` hash
61
+ - Example: `examples/17_skills.rb` — SRE incident response system with flat and recursive skills
62
+ - 20 new tests covering expansion, ordering, cycles, config cascade, and factory passthrough
63
+ - **Streaming content callback (`on_content:`)** — wire streaming at robot build time
64
+ - Stored callback fires on every `run()` call automatically
65
+ - Pass-through block on `run()` for per-call streaming
66
+ - When both exist, both fire (stored first, then runtime block)
67
+ - `effective_streaming_block` merges stored + runtime into a single Proc
68
+ - Added `:on_content` to `RunConfig::CALLBACK_FIELDS`
69
+ - 12 new tests
70
+ - **Graceful tool error handling** — `RobotLab::Tool#call` wraps `execute` with `rescue StandardError`
71
+ - Errors returned as plain string (`"Error (tool_name): message"`) so the LLM can reason about them
72
+ - Class-level `raise_on_error` opt-out for critical tools
73
+ - MCP tools inherit the same wrapper via `Tool.create` subclasses
74
+ - 10 new tests
75
+ - **`RobotRunJob` generator template** (`job.rb.tt`) — turnkey ActiveJob background job for robot runs
76
+ - Resolves robot class via `constantize.build`
77
+ - Wires `TurboStreamCallbacks` when `turbo-rails` is available (graceful no-op otherwise)
78
+ - Persists results via `result.export`; broadcasts completion/error via Turbo Streams
79
+ - `--skip-job` option on install generator
80
+ - **`TurboStreamCallbacks` module** (`lib/robot_lab/rails_integration/turbo_stream_callbacks.rb`)
81
+ - `available?` — runtime check for `Turbo::StreamsChannel`
82
+ - `build_content_callback(stream_name:, target:)` — broadcasts HTML-escaped content chunks
83
+ - `build_tool_call_callback(stream_name:, target:)` — broadcasts tool call badges
84
+ - 13 tests
85
+ - **Rails demo app** (`examples/18_rails/`) — minimal hand-built Rails 8 app exercising all Rails integration
86
+ - ChatRobot with TimeTool, RobotRunJob, Turbo Stream token streaming, SQLite persistence
87
+ - No asset pipeline — Turbo JS via importmap from CDN
88
+ - `:async` adapters for both ActiveJob and ActionCable (no Redis, no Solid Queue)
89
+ - User messages persisted in history; auto-scrolling via MutationObserver; form clears after submit
90
+ - Rake tasks: `examples:rails_setup`, `examples:rails`
91
+ - **Routing robot example** in Rails integration docs — `ClassifierRobot` subclass with `call(result)` override
92
+ - **Custom tool example** in Rails integration docs — `OrderLookup` tool with ActiveRecord
93
+
94
+ ### Changed
95
+
96
+ - Bumped version to 0.0.8
97
+ - **Renamed `RobotLab::Rails` → `RobotLab::RailsIntegration`** — eliminates constant shadowing where bare `Rails` inside `module RobotLab` resolved to the gem's own namespace instead of `::Rails`
98
+ - Moved `lib/robot_lab/rails/` → `lib/robot_lab/rails_integration/`
99
+ - Updated all require paths, loader.ignore, generator templates, tests, and documentation
100
+ - Reverted `::Rails` back to bare `Rails` in `config.rb` (shadow eliminated)
101
+ - Rakefile updated with `STANDALONE_APPS` map for standalone demo apps
102
+ - Documentation updates across README, guides, API reference, and examples for all new features
103
+ - Updated Gemfile.lock dependencies
104
+
105
+ ### Fixed
106
+
107
+ - `RobotLab::Rails` namespace shadowing `::Rails` in `config.rb` (`NoMethodError: undefined method 'root' for module RobotLab::Rails`)
108
+ - MkDocs broken anchor link in `docs/examples/index.md` (`#with-conversation-history` → `#with-memory`)
109
+
14
110
  ## [0.0.7] - 2026-02-17 [unreleased]
15
111
 
16
112
  ### Added
data/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # RobotLab
2
2
 
3
- > [!CAUTION]
4
- > This gem is under active development. APIs and features may change without notice. See the [CHANGELOG](CHANGELOG.md) for details.
3
+ > [!INFO]
4
+ > See the [CHANGELOG](CHANGELOG.md) for the latest changes. The [examples directory has a good cross section of demo apps](examples/README.md) that show-off the various capabilities of the RobotLab library.
5
+
5
6
  <br>
6
7
  <table>
7
8
  <tr>
@@ -10,24 +11,28 @@
10
11
  <em>"Build robots. Solve problems."</em>
11
12
  </td>
12
13
  <td width="50%" valign="top">
13
- <strong>Multi-robot LLM workflow orchestration for Ruby</strong><br><br>
14
- RobotLab enables you to build sophisticated AI applications using multiple specialized robots (LLM agents) that work together to accomplish complex tasks. Each robot has its own system prompt, tools, and capabilities.<br><br>
15
14
  <strong>Key Features</strong><br>
16
15
 
17
16
  - <strong>Multi-Robot Architecture</strong> - Build with specialized AI agents<br>
18
17
  - <strong>Network Orchestration</strong> - Connect robots with flexible routing<br>
19
- - <strong>Extensible Tools</strong> - Give robots custom capabilities<br>
20
- - <strong>MCP Integration</strong> - Connect to external tool servers<br>
18
+ - <strong>Prompt Templates</strong> - Self-contained .md files with YAML front matter<br>
19
+ - <strong>Composable Skills</strong> - Mix reusable prompt behaviors into any robot<br>
20
+ - <strong>Extensible Tools</strong> - Custom capabilities with graceful error handling<br>
21
+ - <strong>Human-in-the-Loop</strong> - AskUser tool for interactive prompting<br>
22
+ - <strong>Content Streaming</strong> - Stored callbacks, per-call blocks, or both<br>
23
+ - <strong>MCP Integration</strong> - Connect to external tool servers with timeouts and retry<br>
24
+ - <strong>Local LLM Providers</strong> - Ollama, GPUStack, LM Studio via provider passthrough<br>
21
25
  - <strong>Shared Memory</strong> - Reactive key-value store with subscriptions<br>
22
- - <strong>Conversation History</strong> - Persist and restore threads<br>
23
26
  - <strong>Message Bus</strong> - Bidirectional robot communication via TypedBus<br>
24
27
  - <strong>Dynamic Spawning</strong> - Robots create new robots at runtime<br>
25
- - <strong>Streaming</strong> - Real-time event streaming<br>
26
- - <strong>Rails Integration</strong> - Generators and ActiveRecord support
28
+ - <strong>Layered Configuration</strong> - Cascading YAML, env vars, and RunConfig<br>
29
+ - <strong>Rails Integration</strong> - Generators, background jobs, Turbo Stream broadcasting
27
30
  </td>
28
31
  </tr>
29
32
  </table>
30
33
 
34
+ <p>RobotLab enables sophisticated AI applications using multiple specialized robots (LLM agents) that work together to accomplish complex tasks. Each robot has its own instructions, skills, tools, and capabilities. Review the [full documentation website](https://madbomber.github.io/robot_lab) snd explore the [many examples](examples/README.md) available as working demo applications.</p>
35
+
31
36
  ## Installation
32
37
 
33
38
  ```bash
@@ -67,6 +72,19 @@ puts result.last_text_content
67
72
  # => "The capital of France is Paris."
68
73
  ```
69
74
 
75
+ ### Local LLM Providers
76
+
77
+ For local LLM providers (Ollama, GPUStack, LM Studio, etc.), use the `provider:` parameter:
78
+
79
+ ```ruby
80
+ robot = RobotLab.build(
81
+ name: "local_bot",
82
+ model: "llama3.2",
83
+ provider: :ollama,
84
+ system_prompt: "You are a helpful assistant."
85
+ )
86
+ ```
87
+
70
88
  ### Configuration
71
89
 
72
90
  RobotLab uses [MywayConfig](https://github.com/MadBomber/myway_config) for layered configuration. There is no `configure` block. Configuration is loaded automatically from multiple sources in priority order:
@@ -172,7 +190,35 @@ You are a GitHub assistant. Use available tools to help with repository tasks.
172
190
  robot = RobotLab.build(template: :github_assistant)
173
191
  ```
174
192
 
175
- Front matter supports: `description`, `robot_name`, `tools`, `mcp`, `parameters`, and LLM config keys (`model`, `temperature`, `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop`). Constructor-provided values always take precedence over front matter.
193
+ Front matter supports: `description`, `robot_name`, `tools`, `mcp`, `skills`, `parameters`, and LLM config keys (`model`, `temperature`, `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop`). Constructor-provided values always take precedence over front matter.
194
+
195
+ ### Composable Skills
196
+
197
+ Skills let you compose robot behaviors from reusable templates. A skill is just a template whose prompt body is prepended before the main template. Use skills to mix in capabilities like "ask clarifying questions", "respond in JSON", or "follow safety guidelines" without creating a dedicated template for every combination.
198
+
199
+ ```ruby
200
+ # Compose a support robot from reusable skills
201
+ robot = RobotLab.build(
202
+ name: "support",
203
+ template: :support_agent,
204
+ skills: [:clarifier, :sentiment_aware, :json_responder],
205
+ context: { company: "Acme Corp" }
206
+ )
207
+ ```
208
+
209
+ Skills can also be declared in template front matter:
210
+
211
+ ```markdown
212
+ ---
213
+ description: Support agent with built-in skills
214
+ skills:
215
+ - clarifier
216
+ - sentiment_aware
217
+ ---
218
+ You are a support agent for <%= company %>.
219
+ ```
220
+
221
+ Skills are expanded depth-first and can reference other skills (with automatic cycle detection). Config cascades through skills in order — later values override earlier ones, and constructor kwargs always win.
176
222
 
177
223
  ### Combining Templates with System Prompts
178
224
 
@@ -252,6 +298,28 @@ result = robot
252
298
  .run("Write a haiku about Ruby programming")
253
299
  ```
254
300
 
301
+ ## Graceful Tool Error Handling
302
+
303
+ `RobotLab::Tool` automatically catches exceptions in `execute` and returns a plain-text error to the LLM instead of crashing the run. The LLM can then reason about the error and try an alternative approach.
304
+
305
+ ```ruby
306
+ tool = RobotLab::Tool.create(name: "fetch_data") do |args|
307
+ raise IOError, "connection refused"
308
+ end
309
+
310
+ result = tool.call({})
311
+ # => "Error (fetch_data): connection refused"
312
+ ```
313
+
314
+ This applies to all tools — subclasses, factory tools, and MCP tools. For critical tools where you want exceptions to propagate, opt out per class:
315
+
316
+ ```ruby
317
+ class CriticalTool < RobotLab::Tool
318
+ self.raise_on_error = true
319
+ # ...
320
+ end
321
+ ```
322
+
255
323
  ## Creating a Robot with Tools
256
324
 
257
325
  ```ruby
@@ -389,14 +457,15 @@ puts result.value.last_text_content
389
457
  Connect to external tool servers via Model Context Protocol:
390
458
 
391
459
  ```ruby
392
- # Configure MCP server
460
+ # Configure MCP server (with optional timeout)
393
461
  filesystem_server = {
394
462
  name: "filesystem",
395
463
  transport: {
396
464
  type: "stdio",
397
465
  command: "mcp-server-filesystem",
398
466
  args: ["/path/to/allowed/directory"]
399
- }
467
+ },
468
+ timeout: 30 # seconds (default: 15)
400
469
  }
401
470
 
402
471
  # Create robot with MCP server - tools are auto-discovered
@@ -406,10 +475,18 @@ robot = RobotLab.build(
406
475
  mcp: [filesystem_server]
407
476
  )
408
477
 
478
+ # Optionally connect eagerly (default is lazy on first run)
479
+ robot.connect_mcp!
480
+
481
+ # Check connection status
482
+ puts "Failed: #{robot.failed_mcp_server_names}" if robot.failed_mcp_server_names.any?
483
+
409
484
  # Robot can now use filesystem tools
410
485
  result = robot.run("List the files in the current directory")
411
486
  ```
412
487
 
488
+ MCP connections are resilient: failed servers are automatically retried on subsequent `run()` calls, and one failing server does not prevent others from connecting.
489
+
413
490
  ## Message Bus
414
491
 
415
492
  Robots can communicate bidirectionally via an optional message bus, independent of the Network pipeline. This enables negotiation loops, convergence patterns, and cyclic workflows.
@@ -502,26 +579,48 @@ Key features:
502
579
 
503
580
  ## Streaming
504
581
 
505
- Use a `Streaming::Context` with a publish callback to receive real-time events:
582
+ Stream LLM content in real-time using a stored callback, a per-call block, or both. Each receives a [`RubyLLM::Chunk`](https://rubyllm.com/streaming/#basic-streaming) use `chunk.content` for the text delta. Chunks also carry `model_id`, `tool_calls`, `thinking`, and token usage on the final chunk.
583
+
584
+ ### Stored Callback (`on_content:`)
585
+
586
+ Wire streaming at build time. The callback fires on every `run()` call automatically:
506
587
 
507
588
  ```ruby
508
- handler = lambda do |event|
509
- case event[:event]
510
- when RobotLab::Streaming::Events::TEXT_DELTA
511
- print event[:data][:delta]
512
- when RobotLab::Streaming::Events::RUN_COMPLETED
513
- puts "\nDone!"
514
- end
515
- end
589
+ robot = RobotLab.build(
590
+ name: "assistant",
591
+ system_prompt: "You are helpful.",
592
+ on_content: ->(chunk) { print chunk.content }
593
+ )
594
+
595
+ robot.run("Tell me a story") # streams automatically
596
+ ```
597
+
598
+ ### Per-Call Block
599
+
600
+ Pass a block to `run()` for one-off streaming:
601
+
602
+ ```ruby
603
+ robot = RobotLab.build(name: "assistant", system_prompt: "You are helpful.")
604
+
605
+ robot.run("Tell me a story") { |chunk| print chunk.content }
606
+ ```
516
607
 
517
- context = RobotLab::Streaming::Context.new(
518
- run_id: SecureRandom.uuid,
519
- message_id: SecureRandom.uuid,
520
- scope: "robot",
521
- publish: handler
608
+ ### Both Together
609
+
610
+ When both a stored callback and a runtime block are provided, both fire (stored first):
611
+
612
+ ```ruby
613
+ robot = RobotLab.build(
614
+ name: "assistant",
615
+ system_prompt: "You are helpful.",
616
+ on_content: ->(chunk) { log_chunk(chunk.content) }
522
617
  )
618
+
619
+ robot.run("Tell me a story") { |chunk| stream_to_client(chunk.content) }
523
620
  ```
524
621
 
622
+ The `on_content:` callback participates in the RunConfig cascade, so it can be set at the network or config level and inherited by robots.
623
+
525
624
  ## Rails Integration
526
625
 
527
626
  ```bash
data/Rakefile CHANGED
@@ -52,7 +52,12 @@ namespace :examples do
52
52
  "16_writers_room" => "writers_room.rb"
53
53
  }.freeze
54
54
 
55
- desc "Run all examples"
55
+ # Subdirectory demos that are standalone apps (not run via `ruby`)
56
+ STANDALONE_APPS = {
57
+ "18_rails" => { setup: "bin/setup", run: "bin/dev" }
58
+ }.freeze
59
+
60
+ desc "Run all examples (excludes standalone apps like 18_rails)"
56
61
  task :all do
57
62
  # Single-file examples
58
63
  Dir.glob("examples/*.rb").sort.each do |example|
@@ -72,6 +77,15 @@ namespace :examples do
72
77
  puts '=' * 60
73
78
  ruby path
74
79
  end
80
+
81
+ # Remind about standalone apps
82
+ STANDALONE_APPS.each do |dir, commands|
83
+ puts "\n#{'=' * 60}"
84
+ puts "Skipped: examples/#{dir} (standalone app)"
85
+ puts " Setup: cd examples/#{dir} && #{commands[:setup]}"
86
+ puts " Run: cd examples/#{dir} && #{commands[:run]}"
87
+ puts '=' * 60
88
+ end
75
89
  end
76
90
 
77
91
  desc "Run a specific example by number (e.g., rake examples:run[1])"
@@ -89,6 +103,16 @@ namespace :examples do
89
103
  dir = Dir.glob("examples/#{padded}_*/").first
90
104
  if dir
91
105
  dir_name = File.basename(dir)
106
+
107
+ # Check if it's a standalone app
108
+ if STANDALONE_APPS.key?(dir_name)
109
+ commands = STANDALONE_APPS[dir_name]
110
+ puts "Example #{args[:num]} is a standalone app (#{dir_name})."
111
+ puts " Setup: cd examples/#{dir_name} && #{commands[:setup]}"
112
+ puts " Run: cd examples/#{dir_name} && #{commands[:run]}"
113
+ next
114
+ end
115
+
92
116
  entry = SUBDIR_ENTRY_POINTS[dir_name]
93
117
  if entry && File.exist?(File.join(dir, entry))
94
118
  ruby File.join(dir, entry)
@@ -99,6 +123,20 @@ namespace :examples do
99
123
  puts "Example #{args[:num]} not found"
100
124
  end
101
125
  end
126
+
127
+ desc "Setup the Rails demo app (example 18)"
128
+ task :rails_setup do
129
+ Dir.chdir("examples/18_rails") do
130
+ sh "bin/setup"
131
+ end
132
+ end
133
+
134
+ desc "Start the Rails demo app (example 18)"
135
+ task :rails do
136
+ Dir.chdir("examples/18_rails") do
137
+ sh "bin/dev"
138
+ end
139
+ end
102
140
  end
103
141
 
104
142
  namespace :docs do