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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +1 -1
- data/lib/robot_lab/mcp/transports/streamable_http.rb +1 -1
- data/lib/robot_lab/version.rb +1 -1
- metadata +3 -253
- data/.architecture/AGENTS.md +0 -32
- data/.architecture/config.yml +0 -8
- data/.architecture/members.yml +0 -60
- data/.architecture/reviews/feature-free-will.md +0 -490
- data/.architecture/reviews/overall-codebase.md +0 -427
- data/.claude/settings.local.json +0 -57
- data/.codex/config.toml +0 -2
- data/.envrc +0 -6
- data/.github/workflows/deploy-github-pages.yml +0 -52
- data/.irbrc +0 -6
- data/.loki +0 -65
- data/.quality/reek_baseline.txt +0 -43
- data/.rubocop.yml +0 -10
- data/Archspec.rb +0 -44
- data/CLAUDE.md +0 -141
- data/COMMITS.md +0 -196
- data/Rakefile +0 -193
- data/_typos.toml +0 -21
- data/agent2agent_review.md +0 -192
- data/agentf_improvements.md +0 -253
- data/agents.md +0 -14
- data/docs/api/core/index.md +0 -147
- data/docs/api/core/memory.md +0 -578
- data/docs/api/core/network.md +0 -553
- data/docs/api/core/result.md +0 -211
- data/docs/api/core/robot.md +0 -1667
- data/docs/api/core/state.md +0 -145
- data/docs/api/core/tool.md +0 -520
- data/docs/api/errors.md +0 -323
- data/docs/api/hooks.md +0 -469
- data/docs/api/index.md +0 -137
- data/docs/api/mcp/client.md +0 -387
- data/docs/api/mcp/index.md +0 -267
- data/docs/api/mcp/server.md +0 -252
- data/docs/api/mcp/transports.md +0 -339
- data/docs/api/messages/index.md +0 -126
- data/docs/api/messages/text-message.md +0 -170
- data/docs/api/messages/tool-call-message.md +0 -206
- data/docs/api/messages/tool-result-message.md +0 -242
- data/docs/api/messages/user-message.md +0 -232
- data/docs/api/skills.md +0 -423
- data/docs/api/streaming/context.md +0 -280
- data/docs/api/streaming/events.md +0 -185
- data/docs/api/streaming/index.md +0 -156
- data/docs/api/support.md +0 -846
- data/docs/architecture/core-concepts.md +0 -575
- data/docs/architecture/index.md +0 -252
- data/docs/architecture/message-flow.md +0 -366
- data/docs/architecture/network-orchestration.md +0 -478
- data/docs/architecture/robot-execution.md +0 -396
- data/docs/architecture/state-management.md +0 -410
- data/docs/assets/css/custom.css +0 -56
- data/docs/assets/images/robot_lab.jpg +0 -0
- data/docs/concepts.md +0 -641
- data/docs/examples/basic-chat.md +0 -258
- data/docs/examples/index.md +0 -425
- data/docs/examples/mcp-server.md +0 -403
- data/docs/examples/multi-robot-network.md +0 -476
- data/docs/examples/tool-usage.md +0 -468
- data/docs/getting-started/configuration.md +0 -645
- data/docs/getting-started/index.md +0 -56
- data/docs/getting-started/installation.md +0 -197
- data/docs/getting-started/quick-start.md +0 -293
- data/docs/guides/building-robots.md +0 -1187
- data/docs/guides/creating-networks.md +0 -736
- data/docs/guides/hooks.md +0 -1206
- data/docs/guides/index.md +0 -78
- data/docs/guides/knowledge.md +0 -219
- data/docs/guides/mcp-integration.md +0 -740
- data/docs/guides/memory.md +0 -458
- data/docs/guides/observability.md +0 -733
- data/docs/guides/streaming.md +0 -323
- data/docs/guides/using-tools.md +0 -780
- data/docs/index.md +0 -280
- data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
- data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
- data/examples/.envrc +0 -3
- data/examples/01_simple_robot.rb +0 -31
- data/examples/02_tools.rb +0 -109
- data/examples/03_network.rb +0 -118
- data/examples/04_mcp.rb +0 -217
- data/examples/05_streaming.rb +0 -137
- data/examples/06_prompt_templates.rb +0 -325
- data/examples/07_network_memory.rb +0 -313
- data/examples/08_llm_config.rb +0 -162
- data/examples/09_chaining.rb +0 -251
- data/examples/10_memory.rb +0 -315
- data/examples/11_network_introspection.rb +0 -239
- data/examples/12_message_bus.rb +0 -67
- data/examples/13_spawn.rb +0 -85
- data/examples/14_rusty_circuit/.envrc +0 -1
- data/examples/14_rusty_circuit/.gitignore +0 -1
- data/examples/14_rusty_circuit/README.md +0 -1
- data/examples/14_rusty_circuit/comic.rb +0 -149
- data/examples/14_rusty_circuit/display.rb +0 -217
- data/examples/14_rusty_circuit/heckler.rb +0 -65
- data/examples/14_rusty_circuit/open_mic.rb +0 -132
- data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
- data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
- data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
- data/examples/14_rusty_circuit/scout.rb +0 -165
- data/examples/14_rusty_circuit/show.log +0 -234
- data/examples/15_memory_network_and_bus/.envrc +0 -1
- data/examples/15_memory_network_and_bus/README.md +0 -66
- data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
- data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
- data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
- data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
- data/examples/15_memory_network_and_bus/output/combined_article.md +0 -11
- data/examples/15_memory_network_and_bus/output/final_article.md +0 -8
- data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
- data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
- data/examples/15_memory_network_and_bus/output/memory.json +0 -13
- data/examples/15_memory_network_and_bus/output/revision_1.md +0 -29
- data/examples/15_memory_network_and_bus/output/revision_2.md +0 -50
- data/examples/15_memory_network_and_bus/output/revision_3.md +0 -8
- data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
- data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
- data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
- data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
- data/examples/16_writers_room/.envrc +0 -1
- data/examples/16_writers_room/display.rb +0 -158
- data/examples/16_writers_room/output/.gitignore +0 -4
- data/examples/16_writers_room/output/README.md +0 -69
- data/examples/16_writers_room/output/opus_001.md +0 -263
- data/examples/16_writers_room/output/opus_001_notes.log +0 -470
- data/examples/16_writers_room/output/opus_002.md +0 -245
- data/examples/16_writers_room/output/opus_002_notes.log +0 -546
- data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
- data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
- data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
- data/examples/16_writers_room/prompts/writer.md +0 -37
- data/examples/16_writers_room/room.rb +0 -186
- data/examples/16_writers_room/tools.rb +0 -173
- data/examples/16_writers_room/writer.rb +0 -121
- data/examples/16_writers_room/writers_room.rb +0 -256
- data/examples/17_skills.rb +0 -234
- data/examples/18_rails/.envrc +0 -3
- data/examples/18_rails/.gitignore +0 -5
- data/examples/18_rails/Gemfile +0 -11
- data/examples/18_rails/README.md +0 -67
- data/examples/18_rails/Rakefile +0 -4
- data/examples/18_rails/app/controllers/application_controller.rb +0 -4
- data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
- data/examples/18_rails/app/jobs/application_job.rb +0 -4
- data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
- data/examples/18_rails/app/models/application_record.rb +0 -5
- data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
- data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
- data/examples/18_rails/app/robots/chat_robot.rb +0 -25
- data/examples/18_rails/app/tools/time_tool.rb +0 -9
- data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
- data/examples/18_rails/app/views/chat/index.html.erb +0 -67
- data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
- data/examples/18_rails/bin/dev +0 -7
- data/examples/18_rails/bin/rails +0 -6
- data/examples/18_rails/bin/setup +0 -15
- data/examples/18_rails/config/application.rb +0 -33
- data/examples/18_rails/config/cable.yml +0 -2
- data/examples/18_rails/config/database.yml +0 -5
- data/examples/18_rails/config/environment.rb +0 -4
- data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
- data/examples/18_rails/config/routes.rb +0 -6
- data/examples/18_rails/config.ru +0 -4
- data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
- data/examples/19_token_tracking.rb +0 -138
- data/examples/20_circuit_breaker.rb +0 -147
- data/examples/21_learning_loop.rb +0 -181
- data/examples/22_context_compression.rb +0 -172
- data/examples/23_convergence.rb +0 -133
- data/examples/24_structured_delegation.rb +0 -151
- data/examples/25_history_search/conversation.jsonl +0 -30
- data/examples/25_history_search.rb +0 -133
- data/examples/26_document_store/api_versioning_adr.md +0 -52
- data/examples/26_document_store/incident_postmortem.md +0 -46
- data/examples/26_document_store/postgres_runbook.md +0 -49
- data/examples/26_document_store/redis_caching_guide.md +0 -48
- data/examples/26_document_store/sidekiq_guide.md +0 -51
- data/examples/26_document_store.rb +0 -140
- data/examples/27_incident_response/README.md +0 -65
- data/examples/27_incident_response/incident_response.rb +0 -261
- data/examples/28_mcp_discovery.rb +0 -113
- data/examples/29_ractor_tools.rb +0 -240
- data/examples/30_ractor_network.rb +0 -252
- data/examples/31_launch_assessment.rb +0 -246
- data/examples/32_newsletter_reader.rb +0 -193
- data/examples/33_stock_generator.rb +0 -80
- data/examples/33_stock_predictor.rb +0 -327
- data/examples/34_agentskills.rb +0 -76
- data/examples/35_hooks.rb +0 -266
- data/examples/README.md +0 -550
- data/examples/common.rb +0 -180
- data/examples/prompts/assistant.md +0 -5
- data/examples/prompts/audit_trail.md +0 -8
- data/examples/prompts/billing.md +0 -10
- data/examples/prompts/classifier.md +0 -7
- data/examples/prompts/comedian.md +0 -6
- data/examples/prompts/comedy_critic.md +0 -10
- data/examples/prompts/configurable.md +0 -9
- data/examples/prompts/dispatcher.md +0 -12
- data/examples/prompts/entity_extractor.md +0 -14
- data/examples/prompts/escalation.md +0 -42
- data/examples/prompts/frontmatter_mcp_test.md +0 -9
- data/examples/prompts/frontmatter_named_test.md +0 -5
- data/examples/prompts/frontmatter_tools_test.md +0 -6
- data/examples/prompts/general.md +0 -7
- data/examples/prompts/github_assistant.md +0 -14
- data/examples/prompts/helper.md +0 -4
- data/examples/prompts/incident_responder.md +0 -18
- data/examples/prompts/keyword_extractor.md +0 -11
- data/examples/prompts/llm_config_demo.md +0 -20
- data/examples/prompts/order_support.md +0 -35
- data/examples/prompts/os_advocate.md +0 -13
- data/examples/prompts/os_chief.md +0 -13
- data/examples/prompts/os_editor.md +0 -13
- data/examples/prompts/parameterized_main_test.md +0 -6
- data/examples/prompts/pii_redactor.md +0 -7
- data/examples/prompts/product_support.md +0 -37
- data/examples/prompts/runbook_protocol.md +0 -12
- data/examples/prompts/sentiment_analyzer.md +0 -12
- data/examples/prompts/skill_a_test.md +0 -4
- data/examples/prompts/skill_b_test.md +0 -4
- data/examples/prompts/skill_config_test.md +0 -5
- data/examples/prompts/skill_cycle_a_test.md +0 -6
- data/examples/prompts/skill_cycle_b_test.md +0 -6
- data/examples/prompts/skill_description_test.md +0 -4
- data/examples/prompts/skill_leaf_test.md +0 -4
- data/examples/prompts/skill_nested_test.md +0 -6
- data/examples/prompts/skill_refs_main_test.md +0 -6
- data/examples/prompts/skill_self_ref_test.md +0 -6
- data/examples/prompts/skill_with_mcp_test.md +0 -9
- data/examples/prompts/skill_with_params_test.md +0 -6
- data/examples/prompts/skill_with_robot_name_test.md +0 -5
- data/examples/prompts/skill_with_tools_test.md +0 -6
- data/examples/prompts/sre_compliance.md +0 -10
- data/examples/prompts/structured_output.md +0 -7
- data/examples/prompts/synthesizer.md +0 -17
- data/examples/prompts/technical.md +0 -10
- data/examples/prompts/template_with_skills_test.md +0 -6
- data/examples/prompts/triage.md +0 -22
- data/examples/ruboruby.md +0 -423
- data/examples/run_all.rb +0 -60
- data/examples/xyzzy.rb +0 -97
- data/logfile +0 -8
- data/mkdocs.yml +0 -209
- data/robot_concurrency.md +0 -38
- data/simple_acp_review.md +0 -298
- data/temp.md +0 -6
- data/tool_manifest_plan.md +0 -155
|
@@ -1,1187 +0,0 @@
|
|
|
1
|
-
# Building Robots
|
|
2
|
-
|
|
3
|
-
This guide covers everything you need to know about creating robots in RobotLab.
|
|
4
|
-
|
|
5
|
-
## Basic Robot
|
|
6
|
-
|
|
7
|
-
Create a robot using the `RobotLab.build` factory method with keyword arguments:
|
|
8
|
-
|
|
9
|
-
```ruby
|
|
10
|
-
robot = RobotLab.build(
|
|
11
|
-
name: "assistant",
|
|
12
|
-
system_prompt: "You are a helpful assistant."
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
result = robot.run("Hello!")
|
|
16
|
-
puts result.last_text_content
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Robot Properties
|
|
20
|
-
|
|
21
|
-
### Name
|
|
22
|
-
|
|
23
|
-
An identifier used for routing, logging, and as the key under which a robot's result is stored in a network's `result.context`. If omitted it defaults to the literal string `"robot"` — nothing is auto-generated, so two unnamed robots share the same name.
|
|
24
|
-
|
|
25
|
-
```ruby
|
|
26
|
-
robot = RobotLab.build(name: "support_agent", system_prompt: "...")
|
|
27
|
-
|
|
28
|
-
RobotLab.build.name # => "robot"
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
> [!NOTE]
|
|
32
|
-
> The default is load-bearing. A robot records whether `name:` was supplied by
|
|
33
|
-
> comparing it against `"robot"`; front-matter `robot_name:` is applied **only**
|
|
34
|
-
> when the constructor left the name at its default. Passing `name: "robot"`
|
|
35
|
-
> explicitly therefore still counts as "not named", and front matter wins.
|
|
36
|
-
|
|
37
|
-
### Description
|
|
38
|
-
|
|
39
|
-
Describes what the robot does (useful for routing decisions):
|
|
40
|
-
|
|
41
|
-
```ruby
|
|
42
|
-
robot = RobotLab.build(
|
|
43
|
-
name: "support_agent",
|
|
44
|
-
description: "Handles customer support inquiries about orders and refunds",
|
|
45
|
-
system_prompt: "..."
|
|
46
|
-
)
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
### Model
|
|
50
|
-
|
|
51
|
-
The LLM model to use. Defaults to the value in `RobotLab.config.ruby_llm.model`:
|
|
52
|
-
|
|
53
|
-
```ruby
|
|
54
|
-
robot = RobotLab.build(
|
|
55
|
-
name: "writer",
|
|
56
|
-
model: "claude-sonnet-4",
|
|
57
|
-
system_prompt: "You are a creative writer."
|
|
58
|
-
)
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### Provider
|
|
62
|
-
|
|
63
|
-
For local LLM providers (Ollama, GPUStack, LM Studio, etc.), use the `provider:` parameter. This tells RubyLLM to skip model validation and connect directly:
|
|
64
|
-
|
|
65
|
-
```ruby
|
|
66
|
-
robot = RobotLab.build(
|
|
67
|
-
name: "local_bot",
|
|
68
|
-
model: "llama3.2",
|
|
69
|
-
provider: :ollama,
|
|
70
|
-
system_prompt: "You are a helpful assistant."
|
|
71
|
-
)
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
When `provider:` is set, `assume_model_exists: true` is automatically applied. The provider is available via `robot.provider`. This context is preserved across every re-application of the robot's config — including when a template's front matter re-renders mid-run — so a local-provider robot doesn't fall back to RubyLLM's static model registry (and raise a "model not found" error) on later turns.
|
|
75
|
-
|
|
76
|
-
Some local models route their entire response through reasoning/thinking content instead of the normal response text (e.g. `qwen3` on Ollama). When that happens, `result.reply` falls back to the thinking text automatically — see [Robot with Local Provider](../api/core/robot.md#robot-with-local-provider) for details.
|
|
77
|
-
|
|
78
|
-
### System Prompt
|
|
79
|
-
|
|
80
|
-
An inline string that defines the robot's personality and behavior:
|
|
81
|
-
|
|
82
|
-
```ruby
|
|
83
|
-
robot = RobotLab.build(
|
|
84
|
-
name: "support",
|
|
85
|
-
system_prompt: <<~PROMPT
|
|
86
|
-
You are a customer support specialist for TechCo.
|
|
87
|
-
|
|
88
|
-
Your responsibilities:
|
|
89
|
-
- Answer questions about products and services
|
|
90
|
-
- Help resolve order issues
|
|
91
|
-
- Provide friendly, professional assistance
|
|
92
|
-
|
|
93
|
-
Always be polite and acknowledge the customer's concerns.
|
|
94
|
-
PROMPT
|
|
95
|
-
)
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
## Template Files
|
|
99
|
-
|
|
100
|
-
Templates are `.md` files managed by [prompt_manager](https://github.com/MadBomber/prompt_manager). Reference a template by symbol; RobotLab resolves it through the configured template path.
|
|
101
|
-
|
|
102
|
-
```ruby
|
|
103
|
-
# Reference template by symbol (loads prompts/support.md)
|
|
104
|
-
robot = RobotLab.build(
|
|
105
|
-
name: "support",
|
|
106
|
-
template: :support,
|
|
107
|
-
context: { company: "TechCo", tone: "friendly" }
|
|
108
|
-
)
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### Template Format
|
|
112
|
-
|
|
113
|
-
Templates use `.md` files with YAML front matter:
|
|
114
|
-
|
|
115
|
-
```markdown title="prompts/support.md"
|
|
116
|
-
---
|
|
117
|
-
description: Customer support assistant
|
|
118
|
-
parameters:
|
|
119
|
-
company: "Acme"
|
|
120
|
-
tone: "professional"
|
|
121
|
-
model: claude-sonnet-4
|
|
122
|
-
temperature: 0.7
|
|
123
|
-
---
|
|
124
|
-
You are a support agent for <%= company %>.
|
|
125
|
-
Your tone should be <%= tone %>.
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
### Front Matter Configuration
|
|
129
|
-
|
|
130
|
-
The following YAML front matter keys are applied to the robot's chat automatically:
|
|
131
|
-
|
|
132
|
-
**LLM Configuration:**
|
|
133
|
-
|
|
134
|
-
| Key | Description | Applied from front matter? |
|
|
135
|
-
|-----|-------------|----------------------------|
|
|
136
|
-
| `model` | Override the LLM model | Yes |
|
|
137
|
-
| `temperature` | Controls randomness (0.0 - 1.0) | Yes |
|
|
138
|
-
| `top_p` | Nucleus sampling threshold | **No — silently dropped** |
|
|
139
|
-
| `top_k` | Top-k sampling | **No — silently dropped** |
|
|
140
|
-
| `max_tokens` | Maximum tokens in response | **No — silently dropped** |
|
|
141
|
-
| `presence_penalty` | Penalize based on presence | **No — silently dropped** |
|
|
142
|
-
| `frequency_penalty` | Penalize based on frequency | **No — silently dropped** |
|
|
143
|
-
| `stop` | Stop sequences | **No — silently dropped** |
|
|
144
|
-
|
|
145
|
-
> [!WARNING]
|
|
146
|
-
> Only `model` and `temperature` take effect from front matter. The other six are
|
|
147
|
-
> parsed into the robot's `RunConfig` and then dropped: `RunConfig#apply_to`
|
|
148
|
-
> dispatches `chat.with_<field>` guarded by `respond_to?`, and `RubyLLM::Chat`
|
|
149
|
-
> only implements `with_model` and `with_temperature`. A template declaring all
|
|
150
|
-
> eight leaves the chat's params hash empty — no warning, no error.
|
|
151
|
-
>
|
|
152
|
-
> The same six **do** work as constructor kwargs or via a `config:` `RunConfig`,
|
|
153
|
-
> which route through `with_params`:
|
|
154
|
-
>
|
|
155
|
-
> ```ruby
|
|
156
|
-
> RobotLab.build(name: "w", system_prompt: "...", top_p: 0.5, max_tokens: 1200)
|
|
157
|
-
> # chat params => {top_p: 0.5, max_tokens: 1200}
|
|
158
|
-
> ```
|
|
159
|
-
|
|
160
|
-
**Robot Identity and Capabilities:**
|
|
161
|
-
|
|
162
|
-
| Key | Description |
|
|
163
|
-
|-----|-------------|
|
|
164
|
-
| `robot_name` | Override the robot's name (when constructor uses the default) |
|
|
165
|
-
| `description` | Human-readable description of the robot |
|
|
166
|
-
| `tools` | Array of tool class names (resolved via `Object.const_get`) |
|
|
167
|
-
| `mcp` | Array of MCP server configurations |
|
|
168
|
-
| `skills` | Array of skill template symbols to prepend (see [Composable Skills](#composable-skills)) |
|
|
169
|
-
|
|
170
|
-
Constructor-provided values always take precedence over frontmatter values.
|
|
171
|
-
|
|
172
|
-
### Rendering a Template to a String
|
|
173
|
-
|
|
174
|
-
`RobotLab.render_template` renders a named template directly to a `String` — using the same configured template library (`prompts_dir` / `ROBOT_LAB_TEMPLATE_PATH`) as `template:` on `RobotLab.build` — without constructing a robot. Front-matter parameters are supplied as keyword arguments:
|
|
175
|
-
|
|
176
|
-
```ruby
|
|
177
|
-
RobotLab.render_template(:objective, topic: "commit messages")
|
|
178
|
-
# => "<rendered body of prompts/objective.md, with topic substituted>"
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
Unlike `template:` on `build` (which renders a template as a robot's *system prompt*), this returns the plain text — useful as a one-off task message, an evaluation rubric, or any other place you want a parameterized `.md` template's body without the overhead of a robot.
|
|
182
|
-
|
|
183
|
-
### Self-Contained Templates
|
|
184
|
-
|
|
185
|
-
Templates can declare everything a robot needs — identity, tools, MCP servers, and LLM config — making the `.md` file a complete robot definition:
|
|
186
|
-
|
|
187
|
-
```markdown title="prompts/my_github_assistant.md"
|
|
188
|
-
---
|
|
189
|
-
description: GitHub assistant with MCP tool access
|
|
190
|
-
robot_name: github_bot
|
|
191
|
-
mcp:
|
|
192
|
-
- name: github
|
|
193
|
-
transport:
|
|
194
|
-
type: stdio
|
|
195
|
-
command: npx
|
|
196
|
-
args: ["-y", "@modelcontextprotocol/server-github"]
|
|
197
|
-
model: claude-sonnet-4
|
|
198
|
-
temperature: 0.3
|
|
199
|
-
---
|
|
200
|
-
You are a helpful GitHub assistant with access to GitHub tools via MCP.
|
|
201
|
-
Use the available tools to help answer questions about GitHub repositories.
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
Build the robot with minimal constructor arguments:
|
|
205
|
-
|
|
206
|
-
```ruby
|
|
207
|
-
# Template provides name, description, MCP config, model, and temperature
|
|
208
|
-
robot = RobotLab.build(template: :my_github_assistant)
|
|
209
|
-
|
|
210
|
-
# MCP still has to be requested at run time — see the warning below
|
|
211
|
-
robot.run("What are the open issues?", mcp: :inherit, tools: :inherit)
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
> [!WARNING]
|
|
215
|
-
> `transport:` **must be a nested hash**. The shipped
|
|
216
|
-
> `examples/prompts/github_assistant.md` uses the flat form
|
|
217
|
-
> (`transport: stdio` with sibling `command:`/`args:` keys), which raises
|
|
218
|
-
> `NoMethodError: undefined method 'transform_keys' for an instance of String`
|
|
219
|
-
> internally. The error is swallowed and logged as a warning, the server lands in
|
|
220
|
-
> `robot.failed_mcp_server_names`, and the robot builds with **zero tools**. That
|
|
221
|
-
> shipped template also declares no `model:` or `temperature:`, so it does not
|
|
222
|
-
> demonstrate the full self-contained pattern shown here.
|
|
223
|
-
|
|
224
|
-
### Tools in Front Matter
|
|
225
|
-
|
|
226
|
-
Declare tool classes by name in the `tools:` key. RobotLab resolves each string to a Ruby constant and instantiates it:
|
|
227
|
-
|
|
228
|
-
```markdown title="prompts/order_support.md"
|
|
229
|
-
---
|
|
230
|
-
description: Order support specialist
|
|
231
|
-
tools:
|
|
232
|
-
- OrderLookup
|
|
233
|
-
- RefundProcessor
|
|
234
|
-
---
|
|
235
|
-
You help customers with order inquiries and refunds.
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
```ruby
|
|
239
|
-
# Tools are loaded from frontmatter — no local_tools: needed
|
|
240
|
-
robot = RobotLab.build(template: :order_support)
|
|
241
|
-
|
|
242
|
-
# ...but they are only sent to the model when the run asks for them
|
|
243
|
-
robot.run("Where is order 12345?", tools: :inherit)
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
Tool classes must be defined and loaded before the robot is built. If a tool name cannot be resolved, it is skipped with a warning.
|
|
247
|
-
|
|
248
|
-
Constructor `local_tools:` overrides frontmatter `tools:` when provided:
|
|
249
|
-
|
|
250
|
-
```ruby
|
|
251
|
-
# Constructor tools take precedence over frontmatter tools
|
|
252
|
-
robot = RobotLab.build(
|
|
253
|
-
template: :order_support,
|
|
254
|
-
local_tools: [OrderLookup] # Only OrderLookup, not RefundProcessor
|
|
255
|
-
)
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
### MCP in Front Matter
|
|
259
|
-
|
|
260
|
-
Declare MCP server configurations directly in the template:
|
|
261
|
-
|
|
262
|
-
```markdown title="prompts/developer.md"
|
|
263
|
-
---
|
|
264
|
-
description: Developer assistant with filesystem access
|
|
265
|
-
mcp:
|
|
266
|
-
- name: filesystem
|
|
267
|
-
transport:
|
|
268
|
-
type: stdio
|
|
269
|
-
command: mcp-server-filesystem
|
|
270
|
-
args: ["--root", "/home/user/projects"]
|
|
271
|
-
---
|
|
272
|
-
You are a developer assistant with filesystem access.
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
```ruby
|
|
276
|
-
robot = RobotLab.build(template: :developer)
|
|
277
|
-
robot.run("List the files in lib/", mcp: :inherit, tools: :inherit)
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
Constructor `mcp:` overrides frontmatter `mcp:` when provided.
|
|
281
|
-
|
|
282
|
-
> [!WARNING]
|
|
283
|
-
> `transport:` takes a nested hash — `type:` plus the transport's own keys. A flat
|
|
284
|
-
> `transport: stdio` with sibling `command:`/`args:` keys fails silently (the
|
|
285
|
-
> `transform_keys` NoMethodError is swallowed) and the robot ends up with no MCP
|
|
286
|
-
> tools. Valid `type:` values are `stdio`, `sse`, `ws`, `websocket`,
|
|
287
|
-
> `streamable-http`, and `http`; the underscored `streamable_http` raises
|
|
288
|
-
> `ArgumentError`.
|
|
289
|
-
|
|
290
|
-
### Template with System Prompt
|
|
291
|
-
|
|
292
|
-
You can combine a template and an inline system prompt. Both are applied to the chat -- the template first, then the system prompt is appended as additional instructions:
|
|
293
|
-
|
|
294
|
-
```ruby
|
|
295
|
-
robot = RobotLab.build(
|
|
296
|
-
name: "support",
|
|
297
|
-
template: :support,
|
|
298
|
-
context: { company: "TechCo" },
|
|
299
|
-
system_prompt: "Always respond in Spanish."
|
|
300
|
-
)
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
## Composable Skills
|
|
304
|
-
|
|
305
|
-
Skills let you compose robot behaviors from reusable templates without creating a dedicated template for every combination. A skill is just a regular template whose prompt body gets prepended before the main template's body.
|
|
306
|
-
|
|
307
|
-
### Why Skills?
|
|
308
|
-
|
|
309
|
-
Consider a support agent that needs to:
|
|
310
|
-
|
|
311
|
-
- Ask clarifying questions before acting
|
|
312
|
-
- Detect customer sentiment
|
|
313
|
-
- Respond in structured JSON
|
|
314
|
-
|
|
315
|
-
Without skills, you'd create a single monolithic template or copy-paste shared instructions across templates. With skills, each behavior is a standalone template that can be mixed into any robot.
|
|
316
|
-
|
|
317
|
-
### Defining a Skill
|
|
318
|
-
|
|
319
|
-
A skill is a standard `.md` template file. There is no special syntax — any template can be used as a skill:
|
|
320
|
-
|
|
321
|
-
```markdown title="prompts/clarifier.md"
|
|
322
|
-
---
|
|
323
|
-
description: Ask clarifying questions before acting
|
|
324
|
-
---
|
|
325
|
-
Before answering, consider whether the user's request is ambiguous.
|
|
326
|
-
If so, ask one focused clarifying question before proceeding.
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
```markdown title="prompts/json_responder.md"
|
|
330
|
-
---
|
|
331
|
-
description: Respond in structured JSON
|
|
332
|
-
temperature: 0.2
|
|
333
|
-
---
|
|
334
|
-
Always respond with valid JSON. Use this structure:
|
|
335
|
-
{"answer": "...", "confidence": 0.0-1.0, "sources": [...]}
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
### Using Skills via Constructor
|
|
339
|
-
|
|
340
|
-
Pass `skills:` as a symbol or array of symbols:
|
|
341
|
-
|
|
342
|
-
```ruby
|
|
343
|
-
# Single skill
|
|
344
|
-
robot = RobotLab.build(
|
|
345
|
-
name: "bot",
|
|
346
|
-
template: :support,
|
|
347
|
-
skills: :clarifier
|
|
348
|
-
)
|
|
349
|
-
|
|
350
|
-
# Multiple skills
|
|
351
|
-
robot = RobotLab.build(
|
|
352
|
-
name: "bot",
|
|
353
|
-
template: :support,
|
|
354
|
-
skills: [:clarifier, :json_responder],
|
|
355
|
-
context: { company: "Acme Corp" }
|
|
356
|
-
)
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
The resulting system prompt is composed in order: clarifier body, then json_responder body, then the main support template body.
|
|
360
|
-
|
|
361
|
-
### Using Skills via Front Matter
|
|
362
|
-
|
|
363
|
-
Templates can declare skills directly in their front matter:
|
|
364
|
-
|
|
365
|
-
```markdown title="prompts/smart_support.md"
|
|
366
|
-
---
|
|
367
|
-
description: Support agent with built-in skills
|
|
368
|
-
skills:
|
|
369
|
-
- clarifier
|
|
370
|
-
- json_responder
|
|
371
|
-
parameters:
|
|
372
|
-
company: null
|
|
373
|
-
---
|
|
374
|
-
You are a support agent for <%= company %>.
|
|
375
|
-
Help customers with their inquiries.
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
```ruby
|
|
379
|
-
# Skills are loaded from front matter automatically
|
|
380
|
-
robot = RobotLab.build(
|
|
381
|
-
template: :smart_support,
|
|
382
|
-
context: { company: "Acme Corp" }
|
|
383
|
-
)
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
Constructor `skills:` and front matter `skills:` are combined — constructor skills are processed first, then front matter skills.
|
|
387
|
-
|
|
388
|
-
### Nested Skills
|
|
389
|
-
|
|
390
|
-
Skills can reference other skills, enabling layered composition:
|
|
391
|
-
|
|
392
|
-
```markdown title="prompts/safety.md"
|
|
393
|
-
---
|
|
394
|
-
description: Safety guidelines
|
|
395
|
-
skills:
|
|
396
|
-
- content_filter
|
|
397
|
-
- pii_redactor
|
|
398
|
-
---
|
|
399
|
-
Follow all safety guidelines when responding.
|
|
400
|
-
```
|
|
401
|
-
|
|
402
|
-
Nested skills are expanded depth-first. For the example above, the prompt order would be: content_filter, pii_redactor, safety, then the main template.
|
|
403
|
-
|
|
404
|
-
### Cycle Detection
|
|
405
|
-
|
|
406
|
-
If skills form a cycle (A references B, B references A), RobotLab detects it automatically, logs a warning, and skips the duplicate. This prevents infinite loops.
|
|
407
|
-
|
|
408
|
-
### Config Cascade
|
|
409
|
-
|
|
410
|
-
Skills can include LLM configuration in their front matter. Config cascades in processing order — later values override earlier ones:
|
|
411
|
-
|
|
412
|
-
```markdown title="prompts/creative_mode.md"
|
|
413
|
-
---
|
|
414
|
-
description: Enable creative responses
|
|
415
|
-
temperature: 0.9
|
|
416
|
-
---
|
|
417
|
-
Be creative and imaginative in your responses.
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
```ruby
|
|
421
|
-
robot = RobotLab.build(
|
|
422
|
-
name: "writer",
|
|
423
|
-
template: :article_writer,
|
|
424
|
-
skills: [:creative_mode]
|
|
425
|
-
)
|
|
426
|
-
# temperature is 0.9 from the skill (unless the main template or constructor overrides it)
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
> [!NOTE]
|
|
430
|
-
> Skill front matter is subject to the same limitation as template front matter:
|
|
431
|
-
> only `model` and `temperature` reach the chat. Adding `top_p: 0.95` to
|
|
432
|
-
> `creative_mode.md` would be parsed and then silently discarded. Set it as a
|
|
433
|
-
> constructor kwarg (`top_p: 0.95`) instead.
|
|
434
|
-
|
|
435
|
-
The precedence order (highest wins):
|
|
436
|
-
|
|
437
|
-
1. Constructor kwargs (`temperature: 0.3`)
|
|
438
|
-
2. Main template front matter
|
|
439
|
-
3. Later skills override earlier skills
|
|
440
|
-
4. First skill in the list
|
|
441
|
-
|
|
442
|
-
### Skills Without a Main Template
|
|
443
|
-
|
|
444
|
-
Skills work without a main template — useful for quick composition:
|
|
445
|
-
|
|
446
|
-
```ruby
|
|
447
|
-
robot = RobotLab.build(
|
|
448
|
-
name: "safe_bot",
|
|
449
|
-
skills: [:safety, :json_responder],
|
|
450
|
-
system_prompt: "You answer questions about our product."
|
|
451
|
-
)
|
|
452
|
-
```
|
|
453
|
-
|
|
454
|
-
### Shared Context
|
|
455
|
-
|
|
456
|
-
All skills and the main template render with the same `context:` hash. Define parameters in each skill's front matter and pass values through the shared context:
|
|
457
|
-
|
|
458
|
-
```markdown title="prompts/branded.md"
|
|
459
|
-
---
|
|
460
|
-
description: Brand-aware responses
|
|
461
|
-
parameters:
|
|
462
|
-
company_name: null
|
|
463
|
-
---
|
|
464
|
-
You represent <%= company_name %>. Always maintain brand voice.
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
```ruby
|
|
468
|
-
robot = RobotLab.build(
|
|
469
|
-
template: :support,
|
|
470
|
-
skills: [:branded],
|
|
471
|
-
context: { company_name: "Acme Corp" } # shared with all skills
|
|
472
|
-
)
|
|
473
|
-
```
|
|
474
|
-
|
|
475
|
-
## Adding Tools
|
|
476
|
-
|
|
477
|
-
Give robots capabilities via the `local_tools:` parameter. Tools can be `RubyLLM::Tool` subclasses or `RobotLab::Tool` instances:
|
|
478
|
-
|
|
479
|
-
```ruby
|
|
480
|
-
robot = RobotLab.build(
|
|
481
|
-
name: "order_assistant",
|
|
482
|
-
system_prompt: "You help customers with orders.",
|
|
483
|
-
local_tools: [OrderLookup, InventoryCheck]
|
|
484
|
-
)
|
|
485
|
-
|
|
486
|
-
result = robot.run("Where is order 12345?", tools: :inherit)
|
|
487
|
-
```
|
|
488
|
-
|
|
489
|
-
> [!WARNING]
|
|
490
|
-
> **`run` defaults to `tools: :none` and `mcp: :none`.** Attaching tools at build
|
|
491
|
-
> time is not enough — a plain `robot.run("...")` sends the model **zero** tools,
|
|
492
|
-
> because an explicit `:none` means "send no tools this turn" rather than "fall
|
|
493
|
-
> back to the attached set". Pass `tools: :inherit` on the call to send the
|
|
494
|
-
> attached tools, and `mcp: :inherit, tools: :inherit` to connect MCP servers and
|
|
495
|
-
> send their tools.
|
|
496
|
-
>
|
|
497
|
-
> For a **standalone** robot, do not pass `tools: :inherit` at *build* time: the
|
|
498
|
-
> parent level is the global config's `:none`, so it resolves to an allowlist of
|
|
499
|
-
> `["none"]`, which matches nothing. Leave `tools:` unset in the constructor.
|
|
500
|
-
>
|
|
501
|
-
> | build `tools:` | run `tools:` | tools sent |
|
|
502
|
-
> |---|---|---|
|
|
503
|
-
> | unset | `:none` (default) | none |
|
|
504
|
-
> | unset | `:inherit` | all attached — **the correct pattern** |
|
|
505
|
-
> | `:inherit` | `:inherit` | none — the standalone trap |
|
|
506
|
-
> | `:none` | `:inherit` | all attached |
|
|
507
|
-
>
|
|
508
|
-
> This table is for a robot run on its own. Inside a **network** whose `config:`
|
|
509
|
-
> sets `tools:`/`mcp:`, build-time `:inherit` is not a trap — it is exactly how a
|
|
510
|
-
> robot opts in to the network-level list, and the parent is that list rather
|
|
511
|
-
> than `:none`. See
|
|
512
|
-
> [Network-Wide Tool and MCP Defaults](creating-networks.md#network-wide-tool-and-mcp-defaults).
|
|
513
|
-
>
|
|
514
|
-
> An explicit array (`tools: [OrderLookup]`) is an **allowlist** over the attached
|
|
515
|
-
> tools; it selects from them and cannot add new ones. Entries must match how the
|
|
516
|
-
> tool was attached — a tool attached as a class matches its class name
|
|
517
|
-
> (`[OrderLookup]`), one attached as an instance matches RubyLLM's derived name
|
|
518
|
-
> (`%w[order_lookup]`). The two forms do not cross-match.
|
|
519
|
-
|
|
520
|
-
See the [Using Tools](using-tools.md) guide for details on defining tools.
|
|
521
|
-
|
|
522
|
-
## MCP Configuration
|
|
523
|
-
|
|
524
|
-
Connect to MCP (Model Context Protocol) servers via the `mcp:` parameter:
|
|
525
|
-
|
|
526
|
-
```ruby
|
|
527
|
-
robot = RobotLab.build(
|
|
528
|
-
name: "coder",
|
|
529
|
-
template: :developer,
|
|
530
|
-
mcp: [
|
|
531
|
-
{
|
|
532
|
-
name: "filesystem",
|
|
533
|
-
transport: { type: "stdio", command: "mcp-server-fs", args: ["--root", "/data"] }
|
|
534
|
-
}
|
|
535
|
-
]
|
|
536
|
-
)
|
|
537
|
-
```
|
|
538
|
-
|
|
539
|
-
MCP configuration supports hierarchical resolution:
|
|
540
|
-
|
|
541
|
-
| Value | Behavior |
|
|
542
|
-
|-------|----------|
|
|
543
|
-
| `:none` | No MCP servers (default) |
|
|
544
|
-
| `:inherit` | Use parent network/config MCP servers |
|
|
545
|
-
| `[...]` | Explicit array of server configurations |
|
|
546
|
-
|
|
547
|
-
`run` also defaults to `mcp: :none`, so the servers configured above are not connected by a plain `run`:
|
|
548
|
-
|
|
549
|
-
```ruby
|
|
550
|
-
robot.run("Read config/database.yml", mcp: :inherit, tools: :inherit)
|
|
551
|
-
```
|
|
552
|
-
|
|
553
|
-
`robot.connect_mcp!` connects eagerly if you want the handshake to happen up front, but a later plain `run()` still sends no tools — `tools: :inherit` is what puts the MCP tools in the request. Connection failures are logged and recorded in `robot.failed_mcp_server_names`; they are never raised.
|
|
554
|
-
|
|
555
|
-
See the [MCP Integration](mcp-integration.md) guide for transport types and advanced patterns.
|
|
556
|
-
|
|
557
|
-
## Chaining Configuration
|
|
558
|
-
|
|
559
|
-
Robots support `with_*` method chaining for runtime reconfiguration. Each method returns `self` for fluent usage:
|
|
560
|
-
|
|
561
|
-
```ruby
|
|
562
|
-
robot = RobotLab.build(name: "bot")
|
|
563
|
-
|
|
564
|
-
result = robot
|
|
565
|
-
.with_instructions("Be concise and direct.")
|
|
566
|
-
.with_temperature(0.9)
|
|
567
|
-
.with_model("claude-sonnet-4")
|
|
568
|
-
.run("Summarize quantum computing in one sentence.")
|
|
569
|
-
```
|
|
570
|
-
|
|
571
|
-
### Available Chain Methods
|
|
572
|
-
|
|
573
|
-
This is the complete set — the LLM-facing methods are delegated dynamically from `RubyLLM::Chat`, and `with_template` / `with_bus` are RobotLab's own:
|
|
574
|
-
|
|
575
|
-
| Method | Description |
|
|
576
|
-
|--------|-------------|
|
|
577
|
-
| `with_model(id)` | Change the LLM model |
|
|
578
|
-
| `with_instructions(text)` | Set system instructions |
|
|
579
|
-
| `with_temperature(val)` | Set temperature |
|
|
580
|
-
| `with_params(**params)` | Set arbitrary provider params (`top_p`, `max_tokens`, …) |
|
|
581
|
-
| `with_context(ctx)` | Set the RubyLLM context |
|
|
582
|
-
| `with_headers(**headers)` | Set extra request headers |
|
|
583
|
-
| `with_tool(tool)` | Add a single tool |
|
|
584
|
-
| `with_tools(*tools)` | Add multiple tools |
|
|
585
|
-
| `with_schema(schema)` | Set structured output schema |
|
|
586
|
-
| `with_thinking(config)` | Enable extended thinking |
|
|
587
|
-
| `with_template(id, **ctx)` | Apply a prompt template |
|
|
588
|
-
| `with_bus(bus)` | Connect to a message bus (creates one if nil) |
|
|
589
|
-
|
|
590
|
-
> [!WARNING]
|
|
591
|
-
> `with_top_p`, `with_top_k`, `with_max_tokens`, `with_presence_penalty`,
|
|
592
|
-
> `with_frequency_penalty`, and `with_stop` **do not exist** — calling any of them
|
|
593
|
-
> raises `NoMethodError`. `RubyLLM::Chat` exposes those knobs through
|
|
594
|
-
> `with_params`, so use either the constructor kwarg or `with_params`:
|
|
595
|
-
>
|
|
596
|
-
> ```ruby
|
|
597
|
-
> robot.with_params(max_tokens: 2000, top_p: 0.3).run("...")
|
|
598
|
-
> # or
|
|
599
|
-
> RobotLab.build(name: "bot", system_prompt: "...", max_tokens: 2000, top_p: 0.3)
|
|
600
|
-
> ```
|
|
601
|
-
|
|
602
|
-
## Running Robots
|
|
603
|
-
|
|
604
|
-
### Standalone
|
|
605
|
-
|
|
606
|
-
Run a robot directly with a string message:
|
|
607
|
-
|
|
608
|
-
```ruby
|
|
609
|
-
result = robot.run("Hello!")
|
|
610
|
-
puts result.last_text_content
|
|
611
|
-
```
|
|
612
|
-
|
|
613
|
-
The `run` method returns a `RobotResult` with:
|
|
614
|
-
|
|
615
|
-
```ruby
|
|
616
|
-
result.last_text_content # => "Hi there! How can I help?"
|
|
617
|
-
result.reply # => alias for last_text_content
|
|
618
|
-
result.output # => [TextMessage] built from the final response text
|
|
619
|
-
result.tool_calls # => Array of tool call results (see note)
|
|
620
|
-
result.robot_name # => "assistant"
|
|
621
|
-
result.stop_reason # => always nil (see note)
|
|
622
|
-
result.input_tokens # => Integer
|
|
623
|
-
result.output_tokens # => Integer
|
|
624
|
-
result.duration # => Float (elapsed seconds, set in pipeline execution)
|
|
625
|
-
result.raw # => raw LLM response object
|
|
626
|
-
```
|
|
627
|
-
|
|
628
|
-
> [!NOTE]
|
|
629
|
-
> `result.tool_calls` is effectively always empty. It is read from the *final*
|
|
630
|
-
> assistant message, and by the time ruby_llm's tool loop has finished that
|
|
631
|
-
> message carries no tool calls. Use `:tool_call` [hooks](hooks.md) or the
|
|
632
|
-
> `on_tool_call:` / `on_tool_result:` callbacks to observe tool activity.
|
|
633
|
-
> Similarly, `result.output` holds only the final response text, not the full
|
|
634
|
-
> turn. There is no `result.content` and no `result.text?`.
|
|
635
|
-
|
|
636
|
-
> [!WARNING]
|
|
637
|
-
> `result.stop_reason` is **always `nil`**. `RubyLLM::Message` does not define
|
|
638
|
-
> `stop_reason`, and `build_result` fills the field with
|
|
639
|
-
> `response.respond_to?(:stop_reason) ? response.stop_reason : nil` — so no
|
|
640
|
-
> provider value ever lands there, and `.compact` drops the key from
|
|
641
|
-
> `result.export` entirely. Do not branch on `"end_turn"`, `"tool_use"`, or
|
|
642
|
-
> `"stop"`. Consequently `result.stopped?` is simply "this result has no tool
|
|
643
|
-
> calls".
|
|
644
|
-
>
|
|
645
|
-
> (`RobotLab::Message::VALID_STOP_REASONS` is `["tool", "stop"]`, but that
|
|
646
|
-
> constant governs the `Message` classes you build yourself, not `RobotResult`.)
|
|
647
|
-
|
|
648
|
-
### With Runtime Memory
|
|
649
|
-
|
|
650
|
-
Inject memory values for a single run:
|
|
651
|
-
|
|
652
|
-
```ruby
|
|
653
|
-
result = robot.run("What's my account status?", memory: { user_id: 123 })
|
|
654
|
-
```
|
|
655
|
-
|
|
656
|
-
### In a Network
|
|
657
|
-
|
|
658
|
-
Run through a network for orchestration:
|
|
659
|
-
|
|
660
|
-
```ruby
|
|
661
|
-
network = RobotLab.create_network(name: "pipeline") do
|
|
662
|
-
task :assistant, robot, depends_on: :none
|
|
663
|
-
end
|
|
664
|
-
|
|
665
|
-
result = network.run(message: "Hello!")
|
|
666
|
-
puts result.value.last_text_content
|
|
667
|
-
```
|
|
668
|
-
|
|
669
|
-
### With Streaming
|
|
670
|
-
|
|
671
|
-
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) object — use `chunk.content` for the text delta. Chunks also carry `model_id`, `tool_calls`, `thinking`, and token usage on the final chunk. See the [Streaming API reference](../api/core/robot.md#streaming) for the full chunk interface.
|
|
672
|
-
|
|
673
|
-
**Stored callback** — wired at build time, fires on every `run()`:
|
|
674
|
-
|
|
675
|
-
```ruby
|
|
676
|
-
robot = RobotLab.build(
|
|
677
|
-
name: "assistant",
|
|
678
|
-
system_prompt: "You are helpful.",
|
|
679
|
-
on_content: ->(chunk) { print chunk.content }
|
|
680
|
-
)
|
|
681
|
-
robot.run("Tell me a story") # streams automatically
|
|
682
|
-
```
|
|
683
|
-
|
|
684
|
-
**Per-call block** — passed to `run()`:
|
|
685
|
-
|
|
686
|
-
```ruby
|
|
687
|
-
robot.run("Tell me a story") { |chunk| print chunk.content }
|
|
688
|
-
```
|
|
689
|
-
|
|
690
|
-
**Both together** — stored fires first, then the block:
|
|
691
|
-
|
|
692
|
-
```ruby
|
|
693
|
-
robot = RobotLab.build(
|
|
694
|
-
name: "assistant",
|
|
695
|
-
system_prompt: "You are helpful.",
|
|
696
|
-
on_content: ->(chunk) { log_chunk(chunk.content) }
|
|
697
|
-
)
|
|
698
|
-
robot.run("Tell me a story") { |chunk| stream_to_client(chunk.content) }
|
|
699
|
-
```
|
|
700
|
-
|
|
701
|
-
`on_content` is also a `RunConfig` field, so it can be supplied through a `config:` on the robot itself rather than as a constructor kwarg:
|
|
702
|
-
|
|
703
|
-
```ruby
|
|
704
|
-
config = RobotLab::RunConfig.new(
|
|
705
|
-
on_content: ->(chunk) { broadcast(chunk.content) }
|
|
706
|
-
)
|
|
707
|
-
robot = RobotLab.build(name: "bot", system_prompt: "...", config: config)
|
|
708
|
-
```
|
|
709
|
-
|
|
710
|
-
> [!WARNING]
|
|
711
|
-
> This only works for the robot's **own** config. A network-level `config:` does
|
|
712
|
-
> **not** supply `on_content` (or any other callback or LLM field) to its member
|
|
713
|
-
> robots — a network propagates only `mcp` and `tools`. Each robot reads
|
|
714
|
-
> `on_content` from its own config at construction time, so streaming callbacks
|
|
715
|
-
> must be set per robot.
|
|
716
|
-
|
|
717
|
-
You can also monitor tool activity via callbacks:
|
|
718
|
-
|
|
719
|
-
```ruby
|
|
720
|
-
robot = RobotLab.build(
|
|
721
|
-
name: "assistant",
|
|
722
|
-
system_prompt: "...",
|
|
723
|
-
on_tool_call: ->(tool_call) { puts "Calling: #{tool_call.name}" },
|
|
724
|
-
on_tool_result: ->(result) { puts "Result: #{result}" }
|
|
725
|
-
)
|
|
726
|
-
```
|
|
727
|
-
|
|
728
|
-
## Robot Patterns
|
|
729
|
-
|
|
730
|
-
### Classifier Robot
|
|
731
|
-
|
|
732
|
-
Route requests to specialized handlers. Subclass `RobotLab::Robot` and override `call` for custom pipeline behavior:
|
|
733
|
-
|
|
734
|
-
```ruby
|
|
735
|
-
class ClassifierRobot < RobotLab::Robot
|
|
736
|
-
def call(result)
|
|
737
|
-
context = extract_run_context(result)
|
|
738
|
-
message = context.delete(:message)
|
|
739
|
-
robot_result = run(message, **context)
|
|
740
|
-
|
|
741
|
-
new_result = result
|
|
742
|
-
.with_context(@name.to_sym, robot_result)
|
|
743
|
-
.continue(robot_result)
|
|
744
|
-
|
|
745
|
-
category = robot_result.last_text_content.to_s.strip.downcase
|
|
746
|
-
|
|
747
|
-
case category
|
|
748
|
-
when /billing/ then new_result.activate(:billing)
|
|
749
|
-
when /technical/ then new_result.activate(:technical)
|
|
750
|
-
else new_result.activate(:general)
|
|
751
|
-
end
|
|
752
|
-
end
|
|
753
|
-
end
|
|
754
|
-
|
|
755
|
-
classifier = ClassifierRobot.new(
|
|
756
|
-
name: "classifier",
|
|
757
|
-
system_prompt: <<~PROMPT
|
|
758
|
-
Classify the user's message into exactly one category:
|
|
759
|
-
- billing
|
|
760
|
-
- technical
|
|
761
|
-
- general
|
|
762
|
-
Respond with only the category name, nothing else.
|
|
763
|
-
PROMPT
|
|
764
|
-
)
|
|
765
|
-
```
|
|
766
|
-
|
|
767
|
-
### Specialist Robot
|
|
768
|
-
|
|
769
|
-
Handle specific domains with template and tools:
|
|
770
|
-
|
|
771
|
-
```ruby
|
|
772
|
-
billing_specialist = RobotLab.build(
|
|
773
|
-
name: "billing_specialist",
|
|
774
|
-
description: "Handles billing and payment inquiries",
|
|
775
|
-
template: :billing,
|
|
776
|
-
context: { department: "billing" },
|
|
777
|
-
local_tools: [InvoiceLookup, RefundProcessor]
|
|
778
|
-
)
|
|
779
|
-
|
|
780
|
-
billing_specialist.run("Refund order 12345", tools: :inherit)
|
|
781
|
-
```
|
|
782
|
-
|
|
783
|
-
In a network, the equivalent opt-in is on the task: `task :billing, billing_specialist, tools: :inherit, depends_on: :optional`.
|
|
784
|
-
|
|
785
|
-
### Summarizer Robot
|
|
786
|
-
|
|
787
|
-
Condense information:
|
|
788
|
-
|
|
789
|
-
```ruby
|
|
790
|
-
summarizer = RobotLab.build(
|
|
791
|
-
name: "summarizer",
|
|
792
|
-
description: "Summarizes conversations and documents",
|
|
793
|
-
system_prompt: <<~PROMPT
|
|
794
|
-
Create concise summaries of the provided content.
|
|
795
|
-
Focus on key points and actionable items.
|
|
796
|
-
Use bullet points for clarity.
|
|
797
|
-
PROMPT
|
|
798
|
-
)
|
|
799
|
-
```
|
|
800
|
-
|
|
801
|
-
### Bus-Connected Robot
|
|
802
|
-
|
|
803
|
-
Enable bidirectional communication between robots using a message bus. This pattern supports negotiation loops and convergence:
|
|
804
|
-
|
|
805
|
-
```ruby
|
|
806
|
-
bus = TypedBus::MessageBus.new
|
|
807
|
-
|
|
808
|
-
class Comedian < RobotLab::Robot
|
|
809
|
-
def initialize(bus:)
|
|
810
|
-
super(name: "bob", template: :comedian, bus: bus)
|
|
811
|
-
on_message do |message|
|
|
812
|
-
joke = run(message.content.to_s).last_text_content.strip
|
|
813
|
-
send_reply(to: message.from.to_sym, content: joke, in_reply_to: message.key)
|
|
814
|
-
end
|
|
815
|
-
end
|
|
816
|
-
end
|
|
817
|
-
|
|
818
|
-
class ComedyCritic < RobotLab::Robot
|
|
819
|
-
def initialize(bus:)
|
|
820
|
-
super(name: "alice", template: :comedy_critic, bus: bus)
|
|
821
|
-
@accepted = false
|
|
822
|
-
on_message do |message|
|
|
823
|
-
verdict = run("Evaluate: #{message.content}").last_text_content.strip
|
|
824
|
-
@accepted = verdict.start_with?("FUNNY")
|
|
825
|
-
send_message(to: :bob, content: "Try again.") unless @accepted
|
|
826
|
-
end
|
|
827
|
-
end
|
|
828
|
-
attr_reader :accepted
|
|
829
|
-
end
|
|
830
|
-
|
|
831
|
-
bob = Comedian.new(bus: bus)
|
|
832
|
-
alice = ComedyCritic.new(bus: bus)
|
|
833
|
-
alice.send_message(to: :bob, content: "Tell me a funny robot joke.")
|
|
834
|
-
```
|
|
835
|
-
|
|
836
|
-
The `on_message` block arity controls delivery handling:
|
|
837
|
-
- **1 argument** `|message|` — auto-acknowledges before calling
|
|
838
|
-
- **2 arguments** `|delivery, message|` — manual `delivery.ack!` / `delivery.nack!`
|
|
839
|
-
|
|
840
|
-
See [Message Bus](../architecture/core-concepts.md#message-bus) for details.
|
|
841
|
-
|
|
842
|
-
### Auto-Responding to Bus Tasks
|
|
843
|
-
|
|
844
|
-
The `Comedian`/`ComedyCritic` example above hand-wires `on_message` to run and reply. For the common case — run every inbound task through the robot and reply with the result — `respond_to_tasks`/`serve` do it in one call:
|
|
845
|
-
|
|
846
|
-
```ruby
|
|
847
|
-
bob = RobotLab.build(name: "bob", template: :comedian, bus: bus)
|
|
848
|
-
bob.serve # every inbound task runs through bob.run and replies automatically
|
|
849
|
-
|
|
850
|
-
alice.send_message(to: :bob, content: "Tell me a funny robot joke.")
|
|
851
|
-
```
|
|
852
|
-
|
|
853
|
-
`serve` is shorthand for `respond_to_tasks(auto_reply: true) { |message| run(bus_task_content(message)).reply }`.
|
|
854
|
-
|
|
855
|
-
`bus_task_content` flattens the message payload for `run`: a `String` content is passed through via `to_s`, while a `Hash` content becomes one `"key: value"` line per entry. So a task sent as `{ topic: "robots", style: "dry" }` reaches the LLM as:
|
|
856
|
-
|
|
857
|
-
```
|
|
858
|
-
topic: robots
|
|
859
|
-
style: dry
|
|
860
|
-
```
|
|
861
|
-
|
|
862
|
-
Use `respond_to_tasks` directly when the reply shouldn't just be `run(...).reply` — e.g. to build the prompt yourself, or to post-process the result:
|
|
863
|
-
|
|
864
|
-
```ruby
|
|
865
|
-
bob.respond_to_tasks do |message|
|
|
866
|
-
joke = run(message.content.to_s).reply.strip
|
|
867
|
-
joke.end_with?("!") ? joke : "#{joke}!"
|
|
868
|
-
end
|
|
869
|
-
```
|
|
870
|
-
|
|
871
|
-
Both ignore messages that are themselves replies (`message.reply?`), so two robots calling `serve`/`respond_to_tasks` on each other don't loop.
|
|
872
|
-
|
|
873
|
-
See [Message Bus](../architecture/core-concepts.md#message-bus) for details.
|
|
874
|
-
|
|
875
|
-
### Spawning Robots Dynamically
|
|
876
|
-
|
|
877
|
-
Create new robots at runtime using `spawn`. The bus is created lazily — no upfront wiring required:
|
|
878
|
-
|
|
879
|
-
```ruby
|
|
880
|
-
class Dispatcher < RobotLab::Robot
|
|
881
|
-
attr_reader :spawned
|
|
882
|
-
|
|
883
|
-
def initialize(bus: nil)
|
|
884
|
-
super(name: "dispatcher", template: :dispatcher, bus: bus)
|
|
885
|
-
@spawned = {}
|
|
886
|
-
|
|
887
|
-
on_message do |message|
|
|
888
|
-
puts "#{message.from} replied: #{message.content.to_s.lines.first&.strip}"
|
|
889
|
-
end
|
|
890
|
-
end
|
|
891
|
-
|
|
892
|
-
def dispatch(question)
|
|
893
|
-
# Ask LLM what specialist to create
|
|
894
|
-
plan = run(question).last_text_content.strip
|
|
895
|
-
role, instruction = plan.split("\n", 2)
|
|
896
|
-
role = role.strip.downcase.gsub(/\s+/, "_")
|
|
897
|
-
|
|
898
|
-
# Spawn (or reuse) a specialist
|
|
899
|
-
specialist = @spawned[role] ||= spawn(
|
|
900
|
-
name: role,
|
|
901
|
-
system_prompt: instruction&.strip || "You are a helpful #{role}."
|
|
902
|
-
)
|
|
903
|
-
|
|
904
|
-
# Have the specialist answer and reply
|
|
905
|
-
answer = specialist.run(question).last_text_content.strip
|
|
906
|
-
specialist.send_message(to: :dispatcher, content: answer)
|
|
907
|
-
end
|
|
908
|
-
end
|
|
909
|
-
```
|
|
910
|
-
|
|
911
|
-
Key features of `spawn`:
|
|
912
|
-
|
|
913
|
-
- Creates a child robot on the same bus as the parent
|
|
914
|
-
- Creates a bus lazily if the parent doesn't have one
|
|
915
|
-
- Spawned robots can immediately send and receive messages
|
|
916
|
-
- Multiple robots with the same name enable fan-out messaging
|
|
917
|
-
- The child inherits the parent's `model`/`provider` (caller-supplied `model:`/`provider:` still override) — a dispatcher running on a local Ollama model spawns specialists that also target that model, instead of falling back to the global default model/provider
|
|
918
|
-
|
|
919
|
-
Robots can also join a bus after creation:
|
|
920
|
-
|
|
921
|
-
```ruby
|
|
922
|
-
bot = RobotLab.build(name: "latecomer", system_prompt: "Hello.")
|
|
923
|
-
bot.with_bus(existing_bus) # now connected and can send/receive messages
|
|
924
|
-
```
|
|
925
|
-
|
|
926
|
-
## Context Window Compression
|
|
927
|
-
|
|
928
|
-
Long-running robots accumulate conversation history that can grow to fill the context window. `compress_history` prunes old turns using stemmed term-frequency cosine similarity (term frequencies only — no IDF weighting) against the most recent context, keeping turns that are still relevant and discarding or summarizing those that aren't.
|
|
929
|
-
|
|
930
|
-
```ruby
|
|
931
|
-
# Default settings: protect 3 most-recent turn pairs, drop anything below 0.2
|
|
932
|
-
robot.compress_history
|
|
933
|
-
|
|
934
|
-
# Tune all thresholds
|
|
935
|
-
robot.compress_history(
|
|
936
|
-
recent_turns: 5, # number of recent user+assistant pairs to always keep
|
|
937
|
-
keep_threshold: 0.6, # turns with score >= this are kept verbatim (default 0.6)
|
|
938
|
-
drop_threshold: 0.2 # turns with score < this are dropped (default 0.2)
|
|
939
|
-
)
|
|
940
|
-
```
|
|
941
|
-
|
|
942
|
-
Medium-relevance turns (between thresholds) are dropped by default. Pass a `summarizer:` callable to replace them with a one-sentence summary instead:
|
|
943
|
-
|
|
944
|
-
```ruby
|
|
945
|
-
summarizer = RobotLab.build(name: "summarizer", system_prompt: "Summarize concisely in one sentence.")
|
|
946
|
-
|
|
947
|
-
robot.compress_history(
|
|
948
|
-
summarizer: ->(text) { summarizer.run("Summarize: #{text}").reply }
|
|
949
|
-
)
|
|
950
|
-
```
|
|
951
|
-
|
|
952
|
-
**What is always preserved regardless of score:**
|
|
953
|
-
- System messages
|
|
954
|
-
- Tool call/result message pairs (dropping half would corrupt the conversation)
|
|
955
|
-
- The most recent `recent_turns` user+assistant pairs
|
|
956
|
-
|
|
957
|
-
Requires the `classifier` gem (`~> 2.3`):
|
|
958
|
-
|
|
959
|
-
```ruby
|
|
960
|
-
gem "classifier", "~> 2.3"
|
|
961
|
-
```
|
|
962
|
-
|
|
963
|
-
## Convergence Detection
|
|
964
|
-
|
|
965
|
-
`RobotLab::Convergence` uses stemmed term-frequency cosine similarity (not TF-IDF — on a two-document corpus, IDF suppresses exactly the shared terms that signal agreement) to detect when two independent agents have reached the same conclusion. The primary use case is skipping an expensive reconciler robot when two verifiers already agree. Texts shorter than 30 characters always score `0.0`.
|
|
966
|
-
|
|
967
|
-
```ruby
|
|
968
|
-
# Check the similarity score directly (returns Float 0.0..1.0)
|
|
969
|
-
score = RobotLab::Convergence.similarity(result_a.reply, result_b.reply)
|
|
970
|
-
|
|
971
|
-
# Boolean convergence check (default threshold: 0.85)
|
|
972
|
-
RobotLab::Convergence.detected?(result_a.reply, result_b.reply)
|
|
973
|
-
|
|
974
|
-
# Custom threshold
|
|
975
|
-
RobotLab::Convergence.detected?(text_a, text_b, threshold: 0.75)
|
|
976
|
-
```
|
|
977
|
-
|
|
978
|
-
Wire it into a network for the reconciler fast-path. There is no router object in RobotLab — the reconciler is declared `depends_on: :optional` and a gate robot activates it only when the verifiers disagree:
|
|
979
|
-
|
|
980
|
-
```ruby
|
|
981
|
-
verifier_a = RobotLab.build(name: "verifier_a", system_prompt: "Verify the answer.")
|
|
982
|
-
verifier_b = RobotLab.build(name: "verifier_b", system_prompt: "Independently verify the answer.")
|
|
983
|
-
reconciler = RobotLab.build(name: "reconciler", system_prompt: "Reconcile conflicting answers.")
|
|
984
|
-
|
|
985
|
-
class ConvergenceGate < RobotLab::Robot
|
|
986
|
-
def call(result)
|
|
987
|
-
a = result.context[:verifier_a]&.reply.to_s # keyed by ROBOT name
|
|
988
|
-
b = result.context[:verifier_b]&.reply.to_s
|
|
989
|
-
|
|
990
|
-
return result if RobotLab::Convergence.detected?(a, b) # agree — skip reconciler
|
|
991
|
-
|
|
992
|
-
result.activate(:reconciler)
|
|
993
|
-
end
|
|
994
|
-
end
|
|
995
|
-
|
|
996
|
-
network = RobotLab.create_network(name: "verify") do
|
|
997
|
-
task :verifier_a, verifier_a, depends_on: :none
|
|
998
|
-
task :verifier_b, verifier_b, depends_on: :none
|
|
999
|
-
task :gate, ConvergenceGate.new(name: "gate"), depends_on: %i[verifier_a verifier_b]
|
|
1000
|
-
task :reconciler, reconciler, depends_on: :optional
|
|
1001
|
-
end
|
|
1002
|
-
|
|
1003
|
-
network.run(message: "Is the deployment healthy?").activated_steps
|
|
1004
|
-
# => [] when they agreed, [:reconciler] when they diverged
|
|
1005
|
-
```
|
|
1006
|
-
|
|
1007
|
-
> [!WARNING]
|
|
1008
|
-
> `RobotLab.create_network` accepts only `name:`, `concurrency:`, `config:`, and a
|
|
1009
|
-
> block. There are no `router:` or `robots:` keyword arguments — passing them
|
|
1010
|
-
> raises `ArgumentError: unknown keywords: :robots, :router` — and no `Router` or
|
|
1011
|
-
> `Router::Args` class exists anywhere in the library.
|
|
1012
|
-
|
|
1013
|
-
Requires the `classifier` gem (`~> 2.3`).
|
|
1014
|
-
|
|
1015
|
-
## Structured Delegation
|
|
1016
|
-
|
|
1017
|
-
A robot can delegate a task to another robot using `delegate(to:, task:)`. The result is a `RobotResult` annotated with `delegated_by`, `duration`, and token counts.
|
|
1018
|
-
|
|
1019
|
-
### Synchronous Delegation
|
|
1020
|
-
|
|
1021
|
-
The default: blocks the calling robot until the delegatee finishes.
|
|
1022
|
-
|
|
1023
|
-
```ruby
|
|
1024
|
-
analyst = RobotLab.build(name: "analyst", system_prompt: "Analyze data.")
|
|
1025
|
-
manager = RobotLab.build(name: "manager", system_prompt: "Coordinate work.")
|
|
1026
|
-
|
|
1027
|
-
result = manager.delegate(to: analyst, task: "Summarize the Q3 report.")
|
|
1028
|
-
puts result.reply
|
|
1029
|
-
puts "Completed in %.2fs using %d tokens" % [result.duration, result.output_tokens]
|
|
1030
|
-
puts result.delegated_by # => "manager"
|
|
1031
|
-
```
|
|
1032
|
-
|
|
1033
|
-
### Asynchronous Delegation (Fan-out)
|
|
1034
|
-
|
|
1035
|
-
Pass `async: true` to get a `DelegationFuture` back immediately. Call `.value` to block for the result when you need it.
|
|
1036
|
-
|
|
1037
|
-
```ruby
|
|
1038
|
-
writer = RobotLab.build(name: "writer", system_prompt: "Write clearly.")
|
|
1039
|
-
analyst = RobotLab.build(name: "analyst", system_prompt: "Analyze data.")
|
|
1040
|
-
manager = RobotLab.build(name: "manager", system_prompt: "Coordinate.")
|
|
1041
|
-
|
|
1042
|
-
# Fan out — both start immediately
|
|
1043
|
-
f1 = manager.delegate(to: analyst, task: "Analyze Q3 numbers", async: true)
|
|
1044
|
-
f2 = manager.delegate(to: writer, task: "Draft the intro paragraph", async: true)
|
|
1045
|
-
|
|
1046
|
-
# ... do other work here ...
|
|
1047
|
-
|
|
1048
|
-
# Collect results (blocks if not yet done)
|
|
1049
|
-
analysis = f1.value
|
|
1050
|
-
draft = f2.value
|
|
1051
|
-
|
|
1052
|
-
# With a timeout (raises DelegationFuture::DelegationTimeout)
|
|
1053
|
-
result = f1.value(timeout: 30)
|
|
1054
|
-
```
|
|
1055
|
-
|
|
1056
|
-
`DelegationFuture` API:
|
|
1057
|
-
|
|
1058
|
-
| Method | Description |
|
|
1059
|
-
|--------|-------------|
|
|
1060
|
-
| `resolved?` | Non-blocking poll — true if completed or errored |
|
|
1061
|
-
| `value(timeout: nil)` | Block until done; re-raises any error from the delegatee |
|
|
1062
|
-
| `wait` | Alias for `value` |
|
|
1063
|
-
| `robot_name` | Name of the robot that was delegated to |
|
|
1064
|
-
| `delegated_by` | Name of the robot that created this future |
|
|
1065
|
-
|
|
1066
|
-
## Configuration
|
|
1067
|
-
|
|
1068
|
-
RobotLab uses `MywayConfig` for configuration. Read values off the config object directly:
|
|
1069
|
-
|
|
1070
|
-
```ruby
|
|
1071
|
-
RobotLab.config.ruby_llm.model # => "claude-sonnet-4"
|
|
1072
|
-
RobotLab.config.ruby_llm.request_timeout # => 120
|
|
1073
|
-
```
|
|
1074
|
-
|
|
1075
|
-
`RobotLab.configure` also exists, and yields the config object for imperative setup:
|
|
1076
|
-
|
|
1077
|
-
```ruby
|
|
1078
|
-
RobotLab.configure do |config|
|
|
1079
|
-
config.logger = Logger.new($stdout)
|
|
1080
|
-
end
|
|
1081
|
-
```
|
|
1082
|
-
|
|
1083
|
-
Configuration is layered, lowest precedence first:
|
|
1084
|
-
|
|
1085
|
-
1. Bundled defaults (`lib/robot_lab/config/defaults.yml`)
|
|
1086
|
-
2. Environment-specific overrides (development, test, production)
|
|
1087
|
-
3. XDG user config (`~/.config/robot_lab/robot_lab.yml`)
|
|
1088
|
-
4. Project config (`./config/robot_lab.yml`)
|
|
1089
|
-
5. Environment variables (`ROBOT_LAB_*` prefix; `__` for nesting)
|
|
1090
|
-
6. Constructor parameters
|
|
1091
|
-
|
|
1092
|
-
> [!WARNING]
|
|
1093
|
-
> The XDG file is `~/.config/robot_lab/**robot_lab.yml**` — the filename repeats
|
|
1094
|
-
> the app name. `~/.config/robot_lab/config.yml` is never read.
|
|
1095
|
-
>
|
|
1096
|
-
> A top-level `defaults:` wrapper is **always** ignored — that key means
|
|
1097
|
-
> something only inside the gem's own bundled `defaults.yml`. Write
|
|
1098
|
-
> `max_tool_rounds: 12`, not `defaults:\n max_tool_rounds: 12`.
|
|
1099
|
-
>
|
|
1100
|
-
> An **environment-named** wrapper is a different story, and the two config
|
|
1101
|
-
> files behave differently:
|
|
1102
|
-
>
|
|
1103
|
-
> | File | flat keys | `development:` / `test:` / `production:` wrapper |
|
|
1104
|
-
> |---|---|---|
|
|
1105
|
-
> | `~/.config/robot_lab/robot_lab.yml` | honored | **honored** for the current environment |
|
|
1106
|
-
> | `./config/robot_lab.yml` (no Rails) | honored | ignored |
|
|
1107
|
-
> | `./config/robot_lab.yml` (in Rails) | ignored | **required** — keys must be nested under `Rails.env` |
|
|
1108
|
-
>
|
|
1109
|
-
> The XDG loader checks for a section named for the current environment and only
|
|
1110
|
-
> falls back to the file root when there is none. Outside Rails the environment
|
|
1111
|
-
> defaults to `development` (or `RAILS_ENV` / `RACK_ENV` when set), so a
|
|
1112
|
-
> `development:` section in the XDG file takes effect while `test:` and
|
|
1113
|
-
> `production:` sections sit dormant. Verified with a `max_iterations: 777`
|
|
1114
|
-
> XDG file: flat → 777, `development:` → 777, `production:` → 10 (until
|
|
1115
|
-
> `RACK_ENV=production`, then 777), `defaults:` → 10.
|
|
1116
|
-
>
|
|
1117
|
-
> Inside Rails, `anyway_config` sets the current environment to `Rails.env`,
|
|
1118
|
-
> which makes the **project** file environmental too — a flat
|
|
1119
|
-
> `./config/robot_lab.yml` is then ignored.
|
|
1120
|
-
>
|
|
1121
|
-
> ERB is evaluated only in `./config/robot_lab.yml`. The XDG loader uses
|
|
1122
|
-
> `YAML.safe_load` with no ERB pass, so `<%= ENV['KEY'] %>` there stays a literal
|
|
1123
|
-
> string. Nested env vars also arrive as strings
|
|
1124
|
-
> (`ROBOT_LAB_RUBY_LLM__REQUEST_TIMEOUT=180` yields `"180"`); top-level keys are
|
|
1125
|
-
> type-coerced.
|
|
1126
|
-
|
|
1127
|
-
## Best Practices
|
|
1128
|
-
|
|
1129
|
-
### 1. Clear, Focused Prompts
|
|
1130
|
-
|
|
1131
|
-
```ruby
|
|
1132
|
-
# Good: Specific and focused
|
|
1133
|
-
robot = RobotLab.build(
|
|
1134
|
-
name: "reviewer",
|
|
1135
|
-
system_prompt: <<~PROMPT
|
|
1136
|
-
You are a code reviewer. Review code for:
|
|
1137
|
-
- Security vulnerabilities
|
|
1138
|
-
- Performance issues
|
|
1139
|
-
- Best practice violations
|
|
1140
|
-
|
|
1141
|
-
Provide specific line numbers and suggestions.
|
|
1142
|
-
PROMPT
|
|
1143
|
-
)
|
|
1144
|
-
|
|
1145
|
-
# Bad: Vague and unfocused
|
|
1146
|
-
robot = RobotLab.build(
|
|
1147
|
-
name: "reviewer",
|
|
1148
|
-
system_prompt: "You help with code stuff."
|
|
1149
|
-
)
|
|
1150
|
-
```
|
|
1151
|
-
|
|
1152
|
-
### 2. Compose Behaviors with Skills
|
|
1153
|
-
|
|
1154
|
-
Instead of creating monolithic templates, break behaviors into composable skills:
|
|
1155
|
-
|
|
1156
|
-
```ruby
|
|
1157
|
-
robot = RobotLab.build(
|
|
1158
|
-
name: "support",
|
|
1159
|
-
template: :support,
|
|
1160
|
-
skills: [:clarifier, :safety, :json_responder]
|
|
1161
|
-
)
|
|
1162
|
-
```
|
|
1163
|
-
|
|
1164
|
-
### 3. Use Templates for Reusable Prompts
|
|
1165
|
-
|
|
1166
|
-
Templates keep prompts in version-controlled files and allow parameterization:
|
|
1167
|
-
|
|
1168
|
-
```ruby
|
|
1169
|
-
robot = RobotLab.build(
|
|
1170
|
-
name: "support",
|
|
1171
|
-
template: :support,
|
|
1172
|
-
context: { company: "TechCo", language: "English" }
|
|
1173
|
-
)
|
|
1174
|
-
```
|
|
1175
|
-
|
|
1176
|
-
### 4. Handle Tool Errors Gracefully
|
|
1177
|
-
|
|
1178
|
-
`RobotLab::Tool` automatically catches exceptions and returns plain-text errors to the LLM. For domain-specific error handling, catch known exceptions in `execute` and return structured data. See [Using Tools: Error Handling](using-tools.md#error-handling) for details.
|
|
1179
|
-
|
|
1180
|
-
## Next Steps
|
|
1181
|
-
|
|
1182
|
-
- [Creating Networks](creating-networks.md) - Orchestrate multiple robots
|
|
1183
|
-
- [Message Bus](../architecture/core-concepts.md#message-bus) - Bidirectional robot communication
|
|
1184
|
-
- [Dynamic Spawning](../architecture/core-concepts.md#dynamic-spawning) - Robots creating robots at runtime
|
|
1185
|
-
- [Using Tools](using-tools.md) - Advanced tool patterns
|
|
1186
|
-
- [Memory Guide](memory.md) - Share data between runs and robots
|
|
1187
|
-
- [API Reference: Robot](../api/core/robot.md) - Complete API documentation
|