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
data/docs/api/core/robot.md
DELETED
|
@@ -1,1667 +0,0 @@
|
|
|
1
|
-
# Robot
|
|
2
|
-
|
|
3
|
-
LLM-powered agent with template-based prompts, tools, memory, and MCP integration.
|
|
4
|
-
|
|
5
|
-
## Class Hierarchy
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
RubyLLM::Agent
|
|
9
|
-
└── RobotLab::Robot
|
|
10
|
-
└── Your custom subclasses (e.g., ClassifierRobot)
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
`Robot` inherits from `RubyLLM::Agent`, which creates a persistent `@chat` on initialization. The robot adds template-based prompts, shared memory, hierarchical MCP configuration, and SimpleFlow pipeline integration on top of the base agent.
|
|
14
|
-
|
|
15
|
-
`Robot` also includes `RobotLab::Runnable` and the mixins
|
|
16
|
-
`Robot::TemplateRendering`, `Robot::MCPManagement`, `Robot::BusMessaging`,
|
|
17
|
-
`Robot::HistorySearch`, `Robot::Budget`, and `Robot::Hooking`, and prepends
|
|
18
|
-
`Robot::AgentSkillMatching`.
|
|
19
|
-
|
|
20
|
-
## Constants
|
|
21
|
-
|
|
22
|
-
| Constant | Value | Description |
|
|
23
|
-
|----------|-------|-------------|
|
|
24
|
-
| `Robot::DEFAULT_MAX_TOOLS` | `128` | Ceiling on the number of tools handed to the provider per turn. Override per robot with `RunConfig#max_tools`; a nil, zero, or negative `max_tools` falls back to this default, so the cap cannot be disabled |
|
|
25
|
-
|
|
26
|
-
## Constructor
|
|
27
|
-
|
|
28
|
-
```ruby
|
|
29
|
-
Robot.new(
|
|
30
|
-
name:, # required
|
|
31
|
-
template: nil,
|
|
32
|
-
system_prompt: nil,
|
|
33
|
-
context: {},
|
|
34
|
-
description: nil,
|
|
35
|
-
local_tools: [],
|
|
36
|
-
model: nil,
|
|
37
|
-
provider: nil,
|
|
38
|
-
mcp_servers: [],
|
|
39
|
-
mcp: :none,
|
|
40
|
-
tools: :none,
|
|
41
|
-
on_tool_call: nil,
|
|
42
|
-
on_tool_result: nil,
|
|
43
|
-
on_content: nil,
|
|
44
|
-
enable_cache: true,
|
|
45
|
-
bus: nil,
|
|
46
|
-
skills: nil,
|
|
47
|
-
temperature: nil,
|
|
48
|
-
top_p: nil,
|
|
49
|
-
top_k: nil,
|
|
50
|
-
max_tokens: nil,
|
|
51
|
-
presence_penalty: nil,
|
|
52
|
-
frequency_penalty: nil,
|
|
53
|
-
stop: nil,
|
|
54
|
-
max_tool_rounds: nil,
|
|
55
|
-
token_budget: nil,
|
|
56
|
-
cost_budget: nil,
|
|
57
|
-
doom_loop_threshold: nil,
|
|
58
|
-
mcp_discovery: false,
|
|
59
|
-
config: nil
|
|
60
|
-
)
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
!!! warning "The keyword list is closed"
|
|
64
|
-
`Robot#initialize` has no `**rest`. Any keyword not listed above raises
|
|
65
|
-
`ArgumentError`. In particular `auto_compact:`, `compact_threshold:`,
|
|
66
|
-
`ractor_pool_size:`, `max_concurrent_robots:`, and `max_tools:` are
|
|
67
|
-
**`RunConfig` fields only** — pass them via `config:`, not as constructor
|
|
68
|
-
kwargs. There is no `memory:`, `learn:`, or `learn_domain:` keyword.
|
|
69
|
-
|
|
70
|
-
### Parameters
|
|
71
|
-
|
|
72
|
-
| Name | Type | Default | Description |
|
|
73
|
-
|------|------|---------|-------------|
|
|
74
|
-
| `name` | `String` | **required** | Identifier for the robot. `RobotLab.build` defaults it to the literal string `"robot"`; that default is load-bearing — front-matter `robot_name:` is applied only when the constructor name is still `"robot"` |
|
|
75
|
-
| `template` | `Symbol`, `nil` | `nil` | Prompt template (e.g., `:assistant` loads `prompts/assistant.md`) |
|
|
76
|
-
| `system_prompt` | `String`, `nil` | `nil` | Inline system prompt (appended after template if both given) |
|
|
77
|
-
| `context` | `Hash`, `Proc` | `{}` | Variables passed to the template |
|
|
78
|
-
| `description` | `String`, `nil` | `nil` | Human-readable description of what the robot does |
|
|
79
|
-
| `local_tools` | `Array` | `[]` | Tools defined locally (`RubyLLM::Tool` subclasses or `RobotLab::Tool` instances) |
|
|
80
|
-
| `model` | `String`, `nil` | `nil` | LLM model ID (falls back to `RobotLab.config.ruby_llm.model`) |
|
|
81
|
-
| `provider` | `String`, `Symbol`, `nil` | `nil` | LLM provider for local providers (e.g., `:ollama`, `:gpustack`). Automatically sets `assume_model_exists: true` |
|
|
82
|
-
| `mcp_servers` | `Array` | `[]` | Legacy MCP server configurations |
|
|
83
|
-
| `mcp` | `Symbol`, `Array` | `:none` | Hierarchical MCP config (`:none`, `:inherit`, or server array) |
|
|
84
|
-
| `tools` | `Symbol`, `Array` | `:none` | Hierarchical tools config (`:none`, `:inherit`, or tool name **array**). Must be tool *names* (String/Symbol) — `validate_tools_filter!` raises `ArgumentError` for an instance or class, telling you to use `local_tools:` instead. **For a standalone robot, leave this unset at build time**: `tools: :inherit` here resolves against the global parent `:none` and yields an allowlist that matches nothing. Inside a network whose `config:` sets `tools:`, build-time `:inherit` is the correct way to opt in. See [Runtime Tool Filtering](../../guides/using-tools.md#runtime-tool-filtering) |
|
|
85
|
-
| `on_tool_call` | `Proc`, `nil` | `nil` | Callback invoked when a tool is called |
|
|
86
|
-
| `on_tool_result` | `Proc`, `nil` | `nil` | Callback invoked when a tool returns a result |
|
|
87
|
-
| `on_content` | `Proc`, `nil` | `nil` | Stored streaming callback invoked with each content chunk (see [Streaming](#streaming)) |
|
|
88
|
-
| `enable_cache` | `Boolean` | `true` | Whether to enable semantic caching |
|
|
89
|
-
| `bus` | `TypedBus::MessageBus`, `nil` | `nil` | Optional message bus for inter-robot communication |
|
|
90
|
-
| `skills` | `Symbol`, `Array<Symbol>`, `nil` | `nil` | Skill templates to prepend (see [Skills](#skills)) |
|
|
91
|
-
| `max_tool_rounds` | `Integer`, `nil` | `nil` | Circuit breaker: raise `ToolLoopError` after this many tool calls in one `run()` (see [Tool Loop Circuit Breaker](#tool-loop-circuit-breaker)) |
|
|
92
|
-
| `token_budget` | `Integer`, `nil` | `nil` | Raise `InferenceError` if cumulative tokens exceed this limit after a call; raise `BudgetExceeded` up front if already exhausted (see [Budgets](#budgets)) |
|
|
93
|
-
| `cost_budget` | `Float`, `nil` | `nil` | Same enforcement as `token_budget`, tracked in cumulative dollar cost instead of tokens (requires provider pricing data) |
|
|
94
|
-
| `doom_loop_threshold` | `Integer`, `nil` | `nil` | Tunes the always-on doom-loop detector (default threshold 3). See [Doom Loop Detection](#doom-loop-detection) |
|
|
95
|
-
| `mcp_discovery` | `Boolean` | `false` | When true, the first run narrows the configured MCP server list to those `MCP::ServerDiscovery` judges relevant to the message |
|
|
96
|
-
| `config` | `RunConfig`, `nil` | `nil` | Shared config merged with explicit kwargs (see [RunConfig](#runconfig)) |
|
|
97
|
-
| `temperature` | `Float`, `nil` | `nil` | Controls randomness — applied via `chat.with_temperature` |
|
|
98
|
-
| `top_p` | `Float`, `nil` | `nil` | Nucleus sampling threshold — applied via `chat.with_params` |
|
|
99
|
-
| `top_k` | `Integer`, `nil` | `nil` | Top-k sampling — applied via `chat.with_params` |
|
|
100
|
-
| `max_tokens` | `Integer`, `nil` | `nil` | Maximum tokens in response — applied via `chat.with_params` |
|
|
101
|
-
| `presence_penalty` | `Float`, `nil` | `nil` | Penalize based on presence — applied via `chat.with_params` |
|
|
102
|
-
| `frequency_penalty` | `Float`, `nil` | `nil` | Penalize based on frequency — applied via `chat.with_params` |
|
|
103
|
-
| `stop` | `String`, `Array`, `nil` | `nil` | Stop sequences — applied via `chat.with_params` |
|
|
104
|
-
|
|
105
|
-
When both `config:` and explicit kwargs (e.g., `temperature:`) are provided, explicit kwargs always win.
|
|
106
|
-
|
|
107
|
-
`model` and `temperature` are applied to the chat with dedicated `with_model` /
|
|
108
|
-
`with_temperature` calls. The remaining six LLM fields (`top_p`, `top_k`,
|
|
109
|
-
`max_tokens`, `presence_penalty`, `frequency_penalty`, `stop`) are collected into
|
|
110
|
-
a single `chat.with_params(...)` call. This distinction matters for template
|
|
111
|
-
front matter — see [Templates](#templates).
|
|
112
|
-
|
|
113
|
-
## Factory Method
|
|
114
|
-
|
|
115
|
-
```ruby
|
|
116
|
-
robot = RobotLab.build(
|
|
117
|
-
name: "robot", # Defaults to "robot"
|
|
118
|
-
template: nil,
|
|
119
|
-
system_prompt: nil,
|
|
120
|
-
context: {},
|
|
121
|
-
enable_cache: true,
|
|
122
|
-
bus: nil, # Optional TypedBus::MessageBus
|
|
123
|
-
skills: nil, # Optional skill templates
|
|
124
|
-
config: nil, # Optional RunConfig
|
|
125
|
-
**options # All other Robot.new parameters
|
|
126
|
-
)
|
|
127
|
-
# => RobotLab::Robot
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
If `name` is omitted, it defaults to the literal string `"robot"`. `**options`
|
|
131
|
-
is forwarded verbatim to `Robot.new`, whose keyword list is closed — an unknown
|
|
132
|
-
option raises `ArgumentError`.
|
|
133
|
-
|
|
134
|
-
## Attributes (Read-Only)
|
|
135
|
-
|
|
136
|
-
| Attribute | Type | Description |
|
|
137
|
-
|-----------|------|-------------|
|
|
138
|
-
| `name` | `String` | Unique identifier |
|
|
139
|
-
| `description` | `String`, `nil` | Human-readable description |
|
|
140
|
-
| `template` | `Symbol`, `nil` | Prompt template identifier |
|
|
141
|
-
| `system_prompt` | `String`, `nil` | Inline system prompt |
|
|
142
|
-
| `skills` | `Array<Symbol>`, `nil` | Constructor-provided skill template IDs (nil if none) |
|
|
143
|
-
| `provider` | `String`, `nil` | LLM provider name (e.g., `"ollama"`) — set when using local providers |
|
|
144
|
-
| `local_tools` | `Array` | Locally defined tools |
|
|
145
|
-
| `mcp_clients` | `Hash<String, MCP::Client>` | Connected MCP clients, keyed by server name |
|
|
146
|
-
| `mcp_tools` | `Array<Tool>` | Tools discovered from MCP servers |
|
|
147
|
-
| `memory` | `Memory` | Inherent memory (used when standalone, not in network) |
|
|
148
|
-
| `bus` | `TypedBus::MessageBus`, `nil` | Message bus instance (nil if not configured) |
|
|
149
|
-
| `outbox` | `Hash` | Sent messages tracked by composite key with status and replies |
|
|
150
|
-
| `config` | `RunConfig` | Effective RunConfig (merged from constructor kwargs and passed-in config) |
|
|
151
|
-
| `mcp_config` | `Symbol`, `Array` | Build-time MCP configuration (raw, unresolved) |
|
|
152
|
-
| `tools_config` | `Symbol`, `Array` | Build-time tools configuration (raw, unresolved) |
|
|
153
|
-
| `total_input_tokens` | `Integer` | Cumulative input tokens sent across all `run()` calls |
|
|
154
|
-
| `total_output_tokens` | `Integer` | Cumulative output tokens received across all `run()` calls |
|
|
155
|
-
| `learnings` | `Array<String>` | Accumulated cross-run observations (see [Learning Accumulation](#learning-accumulation)) |
|
|
156
|
-
| `budget_ledger` | `RobotLab::Budget::Ledger`, `nil` | Reserve/reconcile ledger backing `token_budget`/`cost_budget`; `nil` when neither is configured (see [Budgets](#budgets)) |
|
|
157
|
-
| `hooks` | `RobotLab::HookRegistry` | This robot's own hook registry. Populated by [`robot.on`](#on); consulted alongside `RobotLab.hooks` and the network's registry on every run |
|
|
158
|
-
|
|
159
|
-
## Attributes (Read-Write)
|
|
160
|
-
|
|
161
|
-
| Attribute | Type | Default | Description |
|
|
162
|
-
|-----------|------|---------|-------------|
|
|
163
|
-
| `input` | `IO`, `nil` | `nil` | Input stream for user interaction (falls back to `$stdin`) |
|
|
164
|
-
| `output` | `IO`, `nil` | `nil` | Output stream for user interaction (falls back to `$stdout`) |
|
|
165
|
-
|
|
166
|
-
Used by tools like [`AskUser`](tool.md#built-in-askuser) that need terminal IO. Set to `StringIO` for testing.
|
|
167
|
-
|
|
168
|
-
## Methods
|
|
169
|
-
|
|
170
|
-
### run
|
|
171
|
-
|
|
172
|
-
```ruby
|
|
173
|
-
result = robot.run(message = nil, network: nil, task: nil,
|
|
174
|
-
network_memory: nil, network_config: nil, memory: nil,
|
|
175
|
-
mcp: :none, tools: :none, hooks: nil, **kwargs, &block)
|
|
176
|
-
# => RobotResult
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
Primary execution method. Sends a message to the LLM with memory/MCP/tools resolution and returns a `RobotResult`.
|
|
180
|
-
|
|
181
|
-
**Parameters:**
|
|
182
|
-
|
|
183
|
-
| Name | Type | Default | Description |
|
|
184
|
-
|------|------|---------|-------------|
|
|
185
|
-
| `message` | `String`, `nil` | `nil` | The user message to send (positional, optional) |
|
|
186
|
-
| `network` | `Network`, `nil` | `nil` | Network context (passed internally by `Network#run`) |
|
|
187
|
-
| `task` | `Task`, `nil` | `nil` | Task wrapper for the current pipeline step (passed internally); surfaces on hook contexts |
|
|
188
|
-
| `network_memory` | `Memory`, `nil` | `nil` | Shared network memory (passed internally) |
|
|
189
|
-
| `network_config` | `RunConfig`, `nil` | `nil` | Network-level config used when resolving `:inherit` for `mcp`/`tools` (passed internally) |
|
|
190
|
-
| `memory` | `Memory`, `Hash`, `nil` | `nil` | A `Memory` replaces the active memory for this run; a `Hash` is merged into it |
|
|
191
|
-
| `mcp` | `Symbol`, `Array` | `:none` | Runtime MCP override — `:inherit` (all attached servers), `:none`/`[]` (zero this turn), or an explicit array |
|
|
192
|
-
| `tools` | `Symbol`, `Array` | `:none` | Runtime tools override — `:inherit` (all attached tools), `:none`/`[]` (zero this turn), or an explicit name array. See [Runtime Tool Filtering](../../guides/using-tools.md#runtime-tool-filtering) |
|
|
193
|
-
| `hooks` | `Array`, `nil` | `nil` | Per-run hook handler classes, active only for this call |
|
|
194
|
-
| `**kwargs` | `Hash` | `{}` | See below — **not** a passthrough to `Agent#ask` |
|
|
195
|
-
| `&block` | `Proc` | `nil` | Per-call streaming block, receives each content chunk |
|
|
196
|
-
|
|
197
|
-
**What `**kwargs` actually does.** Only `:with` is forwarded to the underlying
|
|
198
|
-
`Agent#ask` (`kwargs.slice(:with)`). *Every other* keyword is treated as
|
|
199
|
-
template re-render context: `kwargs.except(:with)` is merged over the build-time
|
|
200
|
-
context and the template is re-rendered before the call. If the robot has no
|
|
201
|
-
`template:`, those extra keywords are simply ignored.
|
|
202
|
-
|
|
203
|
-
```ruby
|
|
204
|
-
robot = RobotLab.build(name: "support", template: :support)
|
|
205
|
-
robot.run("Help me", company: "Acme") # re-renders the template with company: "Acme"
|
|
206
|
-
robot.run("Describe this", with: image) # forwarded to Agent#ask as attachments
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
When both a stored `on_content` callback and a runtime block are provided, both fire (stored first, then runtime block).
|
|
210
|
-
|
|
211
|
-
!!! warning "`tools:`/`mcp:` default to `:none` here"
|
|
212
|
-
A bare `robot.run(message)` sends **zero** tools and connects **no** MCP
|
|
213
|
-
servers for that call, even when `local_tools:`/`mcp:` were supplied at
|
|
214
|
-
build time. Pass `tools: :inherit` (and/or `mcp: :inherit`) explicitly to
|
|
215
|
-
use what is attached. `mcp: :inherit` triggers the connection attempt;
|
|
216
|
-
`tools: :inherit` is additionally required for the MCP tools to be sent.
|
|
217
|
-
|
|
218
|
-
Each call's resolved tool set *replaces* the chat's tools rather than accumulating, so a subsequent `:none` call correctly clears whatever a prior call attached, and the fully-resolved set is clamped to `max_tools` (`DEFAULT_MAX_TOOLS = 128` by default) right before being handed to the provider — see [Tool Capping](../../guides/using-tools.md#tool-capping-and-per-turn-filtering).
|
|
219
|
-
|
|
220
|
-
**Returns:** `RobotResult`
|
|
221
|
-
|
|
222
|
-
**Examples:**
|
|
223
|
-
|
|
224
|
-
```ruby
|
|
225
|
-
# Simple message — sends no tools, connects no MCP servers
|
|
226
|
-
result = robot.run("What is 2+2?")
|
|
227
|
-
|
|
228
|
-
# Send the tools attached via local_tools:
|
|
229
|
-
result = robot.run("What is 15 * 7?", tools: :inherit)
|
|
230
|
-
|
|
231
|
-
# Connect MCP servers and send their tools
|
|
232
|
-
result = robot.run("Search the repo", mcp: :inherit, tools: :inherit)
|
|
233
|
-
|
|
234
|
-
# Restrict this turn to a named subset
|
|
235
|
-
result = robot.run("Look it up", tools: %w[order_lookup])
|
|
236
|
-
|
|
237
|
-
# With runtime memory
|
|
238
|
-
result = robot.run("Summarize the data", memory: { data: report })
|
|
239
|
-
|
|
240
|
-
# With per-call streaming block
|
|
241
|
-
result = robot.run("Tell me a story") { |chunk| print chunk.content }
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
### model
|
|
245
|
-
|
|
246
|
-
```ruby
|
|
247
|
-
robot.model # => "claude-sonnet-4" or nil
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
Returns the model ID string. Resolves through the underlying chat object.
|
|
251
|
-
|
|
252
|
-
### effective_config
|
|
253
|
-
|
|
254
|
-
```ruby
|
|
255
|
-
robot.effective_config
|
|
256
|
-
# => { model: "claude-sonnet-4-20250514", temperature: 0.7, max_tokens: 4096 }
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
Snapshot of the robot's merged `RunConfig` as a plain Hash, `.compact`ed so unset
|
|
260
|
-
fields are omitted. Reports exactly these keys when set: `model`, `temperature`,
|
|
261
|
-
`top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop`,
|
|
262
|
-
`tools`, `mcp`, `max_tool_rounds`, `doom_loop_threshold`, `auto_compact`,
|
|
263
|
-
`compact_threshold`, `token_budget`, `cost_budget`.
|
|
264
|
-
|
|
265
|
-
This is a *view*, not the config object — use `robot.config` for the `RunConfig` itself.
|
|
266
|
-
|
|
267
|
-
### update
|
|
268
|
-
|
|
269
|
-
```ruby
|
|
270
|
-
robot.update(
|
|
271
|
-
template: nil,
|
|
272
|
-
context: nil,
|
|
273
|
-
system_prompt: nil,
|
|
274
|
-
model: nil,
|
|
275
|
-
temperature: nil,
|
|
276
|
-
**kwargs
|
|
277
|
-
)
|
|
278
|
-
# => self
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
Reconfigure the robot after construction. Returns `self` for chaining.
|
|
282
|
-
|
|
283
|
-
The five named parameters are applied directly (`template` re-renders the prompt;
|
|
284
|
-
`system_prompt`, `model`, and `temperature` call the corresponding `with_*` on the chat).
|
|
285
|
-
|
|
286
|
-
!!! warning "`**kwargs` only reaches fields the chat exposes as `with_<key>`"
|
|
287
|
-
Each extra keyword is forwarded as `@chat.with_#{key}(value)` **only if
|
|
288
|
-
`@chat.respond_to?(:"with_#{key}")`**. `RubyLLM::Chat` has no
|
|
289
|
-
`with_max_tokens`, `with_top_p`, `with_top_k`, `with_stop`,
|
|
290
|
-
`with_presence_penalty`, or `with_frequency_penalty` — so
|
|
291
|
-
`robot.update(max_tokens: 4000)` silently does nothing. Use
|
|
292
|
-
`robot.with_params(max_tokens: 4000)` for those fields.
|
|
293
|
-
|
|
294
|
-
### with_* Methods (Chaining)
|
|
295
|
-
|
|
296
|
-
`with_*` methods are discovered from `RubyLLM::Chat` at construction time and
|
|
297
|
-
defined as singleton methods that delegate to the persistent `@chat` and return
|
|
298
|
-
`self` for chaining. This is the **complete** set:
|
|
299
|
-
|
|
300
|
-
| Method | Description |
|
|
301
|
-
|--------|-------------|
|
|
302
|
-
| `with_model(model_id)` | Change the LLM model |
|
|
303
|
-
| `with_temperature(temp)` | Set temperature |
|
|
304
|
-
| `with_instructions(prompt)` | Set system instructions |
|
|
305
|
-
| `with_tool(tool)` | Add a single tool |
|
|
306
|
-
| `with_tools(*tools)` | Add multiple tools |
|
|
307
|
-
| `with_params(**params)` | Set arbitrary provider parameters |
|
|
308
|
-
| `with_headers(**headers)` | Set custom headers |
|
|
309
|
-
| `with_schema(schema)` | Set output schema |
|
|
310
|
-
| `with_context(**ctx)` | Set context |
|
|
311
|
-
| `with_thinking(opts)` | Enable extended thinking |
|
|
312
|
-
|
|
313
|
-
Plus two defined by RobotLab itself:
|
|
314
|
-
|
|
315
|
-
| Method | Description |
|
|
316
|
-
|--------|-------------|
|
|
317
|
-
| `with_template(id, **context)` | Apply a prompt_manager template (see below) |
|
|
318
|
-
| `with_bus(bus = nil)` | Connect to a message bus (creates one if nil) |
|
|
319
|
-
|
|
320
|
-
!!! danger "These do not exist"
|
|
321
|
-
`with_max_tokens`, `with_top_p`, `with_top_k`, `with_stop`,
|
|
322
|
-
`with_presence_penalty`, and `with_frequency_penalty` are **not** defined
|
|
323
|
-
and raise `NoMethodError`. Set those fields with a constructor kwarg
|
|
324
|
-
(`max_tokens: 2000`) or with `with_params`:
|
|
325
|
-
|
|
326
|
-
```ruby
|
|
327
|
-
robot.with_params(max_tokens: 2000, top_p: 0.3)
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
**Example:**
|
|
331
|
-
|
|
332
|
-
```ruby
|
|
333
|
-
robot = RobotLab.build(name: "bot")
|
|
334
|
-
robot
|
|
335
|
-
.with_model("claude-sonnet-4")
|
|
336
|
-
.with_temperature(0.7)
|
|
337
|
-
.with_params(max_tokens: 2000)
|
|
338
|
-
.with_instructions("Be concise.")
|
|
339
|
-
.run("Hello")
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
### with_template
|
|
343
|
-
|
|
344
|
-
```ruby
|
|
345
|
-
robot.with_template(:assistant, tone: "friendly")
|
|
346
|
-
# => self
|
|
347
|
-
```
|
|
348
|
-
|
|
349
|
-
Apply a prompt_manager template. Separate from the delegated `with_*` methods because it handles template parsing and front matter config.
|
|
350
|
-
|
|
351
|
-
### call
|
|
352
|
-
|
|
353
|
-
```ruby
|
|
354
|
-
robot.call(result)
|
|
355
|
-
# => SimpleFlow::Result
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
SimpleFlow step interface. Extracts the message from `result.context[:run_params]`, calls `run`, and wraps the output in a continued `SimpleFlow::Result`. Automatically records `RobotResult#duration` (elapsed seconds).
|
|
359
|
-
|
|
360
|
-
If the robot raises any exception during execution, the error is caught and wrapped in a `RobotResult` with the error message as content. This ensures one failing robot does not crash the entire network pipeline.
|
|
361
|
-
|
|
362
|
-
Override this method in subclasses for custom routing logic (e.g., classifiers).
|
|
363
|
-
|
|
364
|
-
### reset_memory
|
|
365
|
-
|
|
366
|
-
```ruby
|
|
367
|
-
robot.reset_memory
|
|
368
|
-
# => self
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
Reset the robot's inherent memory to its initial state.
|
|
372
|
-
|
|
373
|
-
### send_message
|
|
374
|
-
|
|
375
|
-
```ruby
|
|
376
|
-
message = robot.send_message(to: :bob, content: "Tell me a joke.")
|
|
377
|
-
# => RobotMessage
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
Publish a message to another robot's bus channel. Increments the internal message counter, creates a `RobotMessage`, tracks it in the outbox, and publishes to the target channel. The counter and outbox mutation are synchronized with an internal mutex, so concurrent `send_message`/`send_reply` calls from multiple threads and reply correlation never clobber each other.
|
|
381
|
-
|
|
382
|
-
**Parameters:**
|
|
383
|
-
|
|
384
|
-
| Name | Type | Description |
|
|
385
|
-
|------|------|-------------|
|
|
386
|
-
| `to` | `String`, `Symbol` | Target robot's channel name |
|
|
387
|
-
| `content` | `String`, `Hash` | Message payload |
|
|
388
|
-
|
|
389
|
-
**Returns:** `RobotMessage`
|
|
390
|
-
|
|
391
|
-
**Raises:** `BusError` if no bus is configured.
|
|
392
|
-
|
|
393
|
-
### send_reply
|
|
394
|
-
|
|
395
|
-
```ruby
|
|
396
|
-
reply = robot.send_reply(to: :alice, content: "Here's a joke...", in_reply_to: "alice:1")
|
|
397
|
-
# => RobotMessage
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
Publish a correlated reply to a specific message. The `in_reply_to` composite key links this reply to the original message.
|
|
401
|
-
|
|
402
|
-
**Parameters:**
|
|
403
|
-
|
|
404
|
-
| Name | Type | Description |
|
|
405
|
-
|------|------|-------------|
|
|
406
|
-
| `to` | `String`, `Symbol` | Target robot's channel name |
|
|
407
|
-
| `content` | `String`, `Hash` | Reply payload |
|
|
408
|
-
| `in_reply_to` | `String` | Composite key of the original message (e.g., `"alice:1"`) |
|
|
409
|
-
|
|
410
|
-
**Returns:** `RobotMessage`
|
|
411
|
-
|
|
412
|
-
**Raises:** `BusError` if no bus is configured.
|
|
413
|
-
|
|
414
|
-
### on_message
|
|
415
|
-
|
|
416
|
-
```ruby
|
|
417
|
-
robot.on_message { |message| puts message.content }
|
|
418
|
-
# => self
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
Register a custom handler for incoming bus messages. Block arity controls delivery handling:
|
|
422
|
-
|
|
423
|
-
- **1 argument** `|message|` — auto-acknowledges the delivery before calling the block
|
|
424
|
-
- **2 arguments** `|delivery, message|` — manual mode; you call `delivery.ack!` or `delivery.nack!`
|
|
425
|
-
|
|
426
|
-
**Examples:**
|
|
427
|
-
|
|
428
|
-
```ruby
|
|
429
|
-
# Auto-ack mode (1 arg)
|
|
430
|
-
robot.on_message do |message|
|
|
431
|
-
joke = run(message.content.to_s).last_text_content
|
|
432
|
-
send_reply(to: message.from.to_sym, content: joke, in_reply_to: message.key)
|
|
433
|
-
end
|
|
434
|
-
|
|
435
|
-
# Manual mode (2 args)
|
|
436
|
-
robot.on_message do |delivery, message|
|
|
437
|
-
if message.content.to_s.length > 10
|
|
438
|
-
delivery.ack!
|
|
439
|
-
send_reply(to: message.from.to_sym, content: "Got it!", in_reply_to: message.key)
|
|
440
|
-
else
|
|
441
|
-
delivery.nack!
|
|
442
|
-
end
|
|
443
|
-
end
|
|
444
|
-
```
|
|
445
|
-
|
|
446
|
-
### respond_to_tasks
|
|
447
|
-
|
|
448
|
-
```ruby
|
|
449
|
-
robot.respond_to_tasks(auto_reply: true) { |message| "the reply content" }
|
|
450
|
-
# => self
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
Auto-answer inbound (non-reply) bus tasks: run the block to produce a reply, and send it back to the sender. This is the symmetric counterpart to how a `robot_lab-cyborg` Cyborg answers its human — one call makes any bus member a first-class responder without hand-wiring `on_message` yourself.
|
|
454
|
-
|
|
455
|
-
**Parameters:**
|
|
456
|
-
|
|
457
|
-
| Name | Type | Default | Description |
|
|
458
|
-
|------|------|---------|-------------|
|
|
459
|
-
| `auto_reply` | `Boolean` | `true` | Send the block's result back to the sender via `send_reply` |
|
|
460
|
-
| `&responder` | `Proc` | **required** | Receives the inbound `message`; return the reply content (`nil` means no reply) |
|
|
461
|
-
|
|
462
|
-
**Returns:** `self`
|
|
463
|
-
|
|
464
|
-
Messages that are themselves replies (`message.reply?`) are ignored, so a two-way `respond_to_tasks` conversation between robots does not loop. The responder runs **inline in the caller's context** — `BusPoller` has no background thread; its `enqueue` either processes the delivery immediately or queues it behind the one in flight and drains it when that finishes. Deliveries to a given robot are therefore handled one at a time, and a long-running responder blocks the sender as well as the next inbound message.
|
|
465
|
-
|
|
466
|
-
```ruby
|
|
467
|
-
bob.respond_to_tasks { |message| "handled: #{message.content}" }
|
|
468
|
-
alice.send_message(to: :bob, content: "ping")
|
|
469
|
-
# bob replies "handled: ping" back to alice automatically
|
|
470
|
-
```
|
|
471
|
-
|
|
472
|
-
### serve
|
|
473
|
-
|
|
474
|
-
```ruby
|
|
475
|
-
robot.serve(auto_reply: true)
|
|
476
|
-
# => self
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
The common case of `respond_to_tasks`: run every inbound task through this robot's own `#run` and reply with the result — the one-call way to make a Robot cooperate on the bus the way a Cyborg already does out of the box.
|
|
480
|
-
|
|
481
|
-
```ruby
|
|
482
|
-
bob.serve
|
|
483
|
-
alice.send_message(to: :bob, content: "Tell me a joke.")
|
|
484
|
-
# bob runs "Tell me a joke." through its LLM and replies with the result
|
|
485
|
-
```
|
|
486
|
-
|
|
487
|
-
Equivalent to `respond_to_tasks(auto_reply: auto_reply) { |message| run(message.content).reply }` (with Hash-content messages flattened to `"key: value"` lines first).
|
|
488
|
-
|
|
489
|
-
### spawn
|
|
490
|
-
|
|
491
|
-
```ruby
|
|
492
|
-
child = robot.spawn(
|
|
493
|
-
name: "specialist",
|
|
494
|
-
system_prompt: "You are a specialist."
|
|
495
|
-
)
|
|
496
|
-
# => RobotLab::Robot (connected to same bus)
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
Create a new robot on the same message bus. If the parent has no bus, one is created automatically and the parent is connected to it.
|
|
500
|
-
|
|
501
|
-
The spawned robot inherits its parent's `model` and `provider` (via `robot.model`/`robot.provider`) so a specialist runs on the same LLM as the robot that spawned it — a robot running on a local Ollama model, for instance, spawns specialists that also target that model rather than falling back to `RobotLab.config.ruby_llm.model` (the global default, typically a cloud model that would fail without credentials). Caller-supplied `model:`/`provider:` in `**options` still override.
|
|
502
|
-
|
|
503
|
-
**Parameters:**
|
|
504
|
-
|
|
505
|
-
| Name | Type | Default | Description |
|
|
506
|
-
|------|------|---------|-------------|
|
|
507
|
-
| `name` | `String` | `"robot"` | Name for the new robot |
|
|
508
|
-
| `system_prompt` | `String`, `nil` | `nil` | Inline system prompt |
|
|
509
|
-
| `template` | `Symbol`, `nil` | `nil` | Prompt template |
|
|
510
|
-
| `local_tools` | `Array` | `[]` | Tools for the new robot |
|
|
511
|
-
| `**options` | `Hash` | `{}` | Additional options passed to `RobotLab.build` |
|
|
512
|
-
|
|
513
|
-
**Returns:** `Robot`
|
|
514
|
-
|
|
515
|
-
**Examples:**
|
|
516
|
-
|
|
517
|
-
```ruby
|
|
518
|
-
# Minimal spawn (bus created automatically)
|
|
519
|
-
bot = RobotLab.build
|
|
520
|
-
bot2 = bot.spawn(system_prompt: "You are helpful.")
|
|
521
|
-
|
|
522
|
-
# Spawn with template
|
|
523
|
-
specialist = dispatcher.spawn(
|
|
524
|
-
name: "billing",
|
|
525
|
-
template: :billing,
|
|
526
|
-
local_tools: [InvoiceLookup]
|
|
527
|
-
)
|
|
528
|
-
|
|
529
|
-
# Fan-out: multiple robots with the same name
|
|
530
|
-
worker1 = bot.spawn(name: "worker", system_prompt: "Worker 1")
|
|
531
|
-
worker2 = bot.spawn(name: "worker", system_prompt: "Worker 2")
|
|
532
|
-
# Messages sent to :worker are delivered to both
|
|
533
|
-
```
|
|
534
|
-
|
|
535
|
-
### assign_bus_poller
|
|
536
|
-
|
|
537
|
-
```ruby
|
|
538
|
-
robot.assign_bus_poller(poller, group: :default)
|
|
539
|
-
# => void — do not rely on the return value
|
|
540
|
-
```
|
|
541
|
-
|
|
542
|
-
Adopt a shared [`BusPoller`](../support.md#robotlabbuspoller) — normally the
|
|
543
|
-
network's. `Network#task` calls this for every robot that responds to it, passing
|
|
544
|
-
the task's `poller_group:`. Any private poller the robot auto-created is dropped
|
|
545
|
-
first.
|
|
546
|
-
|
|
547
|
-
**Parameters:**
|
|
548
|
-
|
|
549
|
-
| Name | Type | Default | Description |
|
|
550
|
-
|------|------|---------|-------------|
|
|
551
|
-
| `poller` | `BusPoller` | **required** | The shared poller to adopt |
|
|
552
|
-
| `group` | `Symbol` | `:default` | Poller group label — informational only; groups share one drain mechanism |
|
|
553
|
-
|
|
554
|
-
You only call this directly when wiring robots onto a shared poller outside a
|
|
555
|
-
`Network`.
|
|
556
|
-
|
|
557
|
-
### inherited_llm_settings
|
|
558
|
-
|
|
559
|
-
```ruby
|
|
560
|
-
robot.inherited_llm_settings
|
|
561
|
-
# => { model: "llama3.2", provider: :ollama }
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
The model/provider pair a [`spawn`](#spawn)ed child inherits from this robot.
|
|
565
|
-
Returns `{}` when neither is set — each key is included only when the
|
|
566
|
-
corresponding reader is truthy. Exposed so an application building children by
|
|
567
|
-
some route other than `spawn` can apply the same inheritance:
|
|
568
|
-
|
|
569
|
-
```ruby
|
|
570
|
-
child = RobotLab.build(name: "helper", bus: parent.bus, **parent.inherited_llm_settings)
|
|
571
|
-
```
|
|
572
|
-
|
|
573
|
-
### rerender_template
|
|
574
|
-
|
|
575
|
-
```ruby
|
|
576
|
-
robot.rerender_template(run_context) # internal — see the warning below
|
|
577
|
-
```
|
|
578
|
-
|
|
579
|
-
Re-renders the robot's template with `run_context` merged over the build-time
|
|
580
|
-
context (skill bodies included, when `skills:` are in play) and reinstalls the
|
|
581
|
-
result as the system prompt, re-appending the inline `system_prompt`. `run` calls
|
|
582
|
-
it automatically when the robot has a `template:` and the call carried extra
|
|
583
|
-
keywords — every keyword except `:with` — which is the mechanism behind
|
|
584
|
-
`robot.run("Help me", company: "Acme")`.
|
|
585
|
-
|
|
586
|
-
`Robot::AgentSkillMatching` overrides it to re-prepend any matched AgentSkill
|
|
587
|
-
instructions afterward, because a re-render replaces the whole system prompt and
|
|
588
|
-
would otherwise discard them mid-run.
|
|
589
|
-
|
|
590
|
-
!!! warning "Public only by accident — treat it as internal"
|
|
591
|
-
`rerender_template` is `private` in `Robot::TemplateRendering`, but the
|
|
592
|
-
prepended `Robot::AgentSkillMatching` redefines it **above** its own
|
|
593
|
-
`private` keyword, so the effective method on `Robot` is public. That is an
|
|
594
|
-
artifact of the override, not a supported entry point: the return value is
|
|
595
|
-
unspecified, and it mutates the chat's system prompt for the rest of the
|
|
596
|
-
conversation. Pass template context to `run` instead.
|
|
597
|
-
|
|
598
|
-
### with_bus
|
|
599
|
-
|
|
600
|
-
```ruby
|
|
601
|
-
robot.with_bus(bus)
|
|
602
|
-
# => self
|
|
603
|
-
```
|
|
604
|
-
|
|
605
|
-
Connect the robot to a message bus after creation. If called without an argument and the robot has no bus, a new one is created. Returns `self` for chaining.
|
|
606
|
-
|
|
607
|
-
**Parameters:**
|
|
608
|
-
|
|
609
|
-
| Name | Type | Default | Description |
|
|
610
|
-
|------|------|---------|-------------|
|
|
611
|
-
| `bus` | `TypedBus::MessageBus`, `nil` | `nil` | Bus to join (creates one if nil and robot has no bus) |
|
|
612
|
-
|
|
613
|
-
**Returns:** `self`
|
|
614
|
-
|
|
615
|
-
**Examples:**
|
|
616
|
-
|
|
617
|
-
```ruby
|
|
618
|
-
# Join an existing bus
|
|
619
|
-
bot = RobotLab.build(name: "bot")
|
|
620
|
-
bot.with_bus(some_bus)
|
|
621
|
-
|
|
622
|
-
# Create a bus on demand
|
|
623
|
-
bot = RobotLab.build(name: "bot").with_bus
|
|
624
|
-
|
|
625
|
-
# Switch buses
|
|
626
|
-
bot.with_bus(bus1) # joins bus1
|
|
627
|
-
bot.with_bus(bus2) # leaves bus1, joins bus2
|
|
628
|
-
```
|
|
629
|
-
|
|
630
|
-
### connect_mcp!
|
|
631
|
-
|
|
632
|
-
```ruby
|
|
633
|
-
robot.connect_mcp!
|
|
634
|
-
# => self
|
|
635
|
-
```
|
|
636
|
-
|
|
637
|
-
Eagerly connect to configured MCP servers and discover tools. Normally MCP connections are lazy (established on first `run`). Call this to connect early, e.g., to display connection status at startup.
|
|
638
|
-
|
|
639
|
-
**Returns:** `self`
|
|
640
|
-
|
|
641
|
-
### failed_mcp_server_names
|
|
642
|
-
|
|
643
|
-
```ruby
|
|
644
|
-
robot.failed_mcp_server_names
|
|
645
|
-
# => Array<String>
|
|
646
|
-
```
|
|
647
|
-
|
|
648
|
-
Returns server names that failed to connect. Useful for displaying connection status or deciding whether to retry.
|
|
649
|
-
|
|
650
|
-
### inject_mcp!
|
|
651
|
-
|
|
652
|
-
```ruby
|
|
653
|
-
robot.inject_mcp!(clients: mcp_clients, tools: mcp_tools)
|
|
654
|
-
# => self
|
|
655
|
-
```
|
|
656
|
-
|
|
657
|
-
Inject pre-connected MCP clients and their tools into this robot. Used by host applications that manage MCP connections externally and need to pass them to robots without re-connecting.
|
|
658
|
-
|
|
659
|
-
**Parameters:**
|
|
660
|
-
|
|
661
|
-
| Name | Type | Description |
|
|
662
|
-
|------|------|-------------|
|
|
663
|
-
| `clients` | `Hash<String, MCP::Client>` | Connected MCP clients keyed by server name |
|
|
664
|
-
| `tools` | `Array<Tool>` | Tools discovered from the MCP servers |
|
|
665
|
-
|
|
666
|
-
**Returns:** `self`
|
|
667
|
-
|
|
668
|
-
**Example:**
|
|
669
|
-
|
|
670
|
-
```ruby
|
|
671
|
-
# Host app manages MCP connections
|
|
672
|
-
clients = { "github" => github_client }
|
|
673
|
-
|
|
674
|
-
# There is no Tool.from_mcp — MCP wrappers are built with Tool.create,
|
|
675
|
-
# exactly as RobotLab's own discover_mcp_tools does.
|
|
676
|
-
tools = github_client.list_tools.map do |tool_def|
|
|
677
|
-
name = tool_def[:name]
|
|
678
|
-
RobotLab::Tool.create(
|
|
679
|
-
name: name,
|
|
680
|
-
description: tool_def[:description],
|
|
681
|
-
parameters: tool_def[:inputSchema],
|
|
682
|
-
mcp: "github"
|
|
683
|
-
) { |args| github_client.call_tool(name, args) }
|
|
684
|
-
end
|
|
685
|
-
|
|
686
|
-
robot.inject_mcp!(clients: clients, tools: tools)
|
|
687
|
-
```
|
|
688
|
-
|
|
689
|
-
### chat
|
|
690
|
-
|
|
691
|
-
```ruby
|
|
692
|
-
robot.chat
|
|
693
|
-
# => RubyLLM::Chat
|
|
694
|
-
```
|
|
695
|
-
|
|
696
|
-
Access the underlying `RubyLLM::Chat` instance. Useful for checkpoint/restore operations that need direct access to conversation state.
|
|
697
|
-
|
|
698
|
-
### messages
|
|
699
|
-
|
|
700
|
-
```ruby
|
|
701
|
-
robot.messages
|
|
702
|
-
# => Array<RubyLLM::Message>
|
|
703
|
-
```
|
|
704
|
-
|
|
705
|
-
Return the conversation messages from the underlying chat.
|
|
706
|
-
|
|
707
|
-
### clear_messages
|
|
708
|
-
|
|
709
|
-
```ruby
|
|
710
|
-
robot.clear_messages(keep_system: true)
|
|
711
|
-
# => self
|
|
712
|
-
```
|
|
713
|
-
|
|
714
|
-
Clear conversation messages, optionally keeping the system prompt.
|
|
715
|
-
|
|
716
|
-
**Parameters:**
|
|
717
|
-
|
|
718
|
-
| Name | Type | Default | Description |
|
|
719
|
-
|------|------|---------|-------------|
|
|
720
|
-
| `keep_system` | `Boolean` | `true` | Whether to preserve the system message |
|
|
721
|
-
|
|
722
|
-
**Returns:** `self`
|
|
723
|
-
|
|
724
|
-
### replace_messages
|
|
725
|
-
|
|
726
|
-
```ruby
|
|
727
|
-
robot.replace_messages(messages)
|
|
728
|
-
# => self
|
|
729
|
-
```
|
|
730
|
-
|
|
731
|
-
Replace conversation messages with a saved set. Useful for checkpoint/restore workflows.
|
|
732
|
-
|
|
733
|
-
**Parameters:**
|
|
734
|
-
|
|
735
|
-
| Name | Type | Description |
|
|
736
|
-
|------|------|-------------|
|
|
737
|
-
| `messages` | `Array<RubyLLM::Message>` | The messages to restore |
|
|
738
|
-
|
|
739
|
-
**Returns:** `self`
|
|
740
|
-
|
|
741
|
-
**Example:**
|
|
742
|
-
|
|
743
|
-
```ruby
|
|
744
|
-
# Save a checkpoint
|
|
745
|
-
saved = robot.messages.dup
|
|
746
|
-
|
|
747
|
-
# ... later, restore it
|
|
748
|
-
robot.replace_messages(saved)
|
|
749
|
-
```
|
|
750
|
-
|
|
751
|
-
### compress_history
|
|
752
|
-
|
|
753
|
-
```ruby
|
|
754
|
-
robot.compress_history(
|
|
755
|
-
recent_turns: 3,
|
|
756
|
-
keep_threshold: 0.6,
|
|
757
|
-
drop_threshold: 0.2,
|
|
758
|
-
summarizer: nil
|
|
759
|
-
)
|
|
760
|
-
# => self
|
|
761
|
-
```
|
|
762
|
-
|
|
763
|
-
Shrink the conversation by scoring each older turn against the most recent
|
|
764
|
-
context and dropping or summarizing the least relevant ones. Internally builds a
|
|
765
|
-
`RobotLab::HistoryCompressor` and hands the result to `replace_messages`.
|
|
766
|
-
|
|
767
|
-
**Parameters:**
|
|
768
|
-
|
|
769
|
-
| Name | Type | Default | Description |
|
|
770
|
-
|------|------|---------|-------------|
|
|
771
|
-
| `recent_turns` | `Integer` | `3` | Turn pairs at the end that are always kept verbatim |
|
|
772
|
-
| `keep_threshold` | `Float` | `0.6` | Cosine score at or above this → kept verbatim |
|
|
773
|
-
| `drop_threshold` | `Float` | `0.2` | Cosine score below this → dropped |
|
|
774
|
-
| `summarizer` | `#call`, `nil` | `nil` | `callable(text) -> String` applied to the medium tier; `nil` drops the medium tier instead |
|
|
775
|
-
|
|
776
|
-
**Returns:** `self`
|
|
777
|
-
|
|
778
|
-
System messages and tool-call/tool-result messages are always preserved.
|
|
779
|
-
|
|
780
|
-
Scoring uses **term-frequency cosine similarity without IDF** (see
|
|
781
|
-
`RobotLab::Convergence`), so it is a lexical overlap measure, not a semantic one.
|
|
782
|
-
|
|
783
|
-
**Raises:** `RobotLab::DependencyError` when the optional `classifier` gem
|
|
784
|
-
(`~> 2.3`) is not installed.
|
|
785
|
-
|
|
786
|
-
```ruby
|
|
787
|
-
robot.compress_history(recent_turns: 5, summarizer: ->(text) { text[0, 200] })
|
|
788
|
-
```
|
|
789
|
-
|
|
790
|
-
`auto_compact: :context_window` on a `RunConfig` calls this automatically before
|
|
791
|
-
an LLM call once estimated tokens exceed `compact_threshold` (default `0.80`) of
|
|
792
|
-
the model's context window. When the `classifier` gem is missing there, the
|
|
793
|
-
`DependencyError` is caught, logged at `:warn`, and compaction is skipped.
|
|
794
|
-
|
|
795
|
-
### delegate
|
|
796
|
-
|
|
797
|
-
```ruby
|
|
798
|
-
result = robot.delegate(to:, task:, async: false, **run_kwargs)
|
|
799
|
-
# => RobotResult (async: false) | DelegationFuture (async: true)
|
|
800
|
-
```
|
|
801
|
-
|
|
802
|
-
Hand a task to another robot and annotate the result with delegation metadata.
|
|
803
|
-
|
|
804
|
-
**Parameters:**
|
|
805
|
-
|
|
806
|
-
| Name | Type | Default | Description |
|
|
807
|
-
|------|------|---------|-------------|
|
|
808
|
-
| `to` | `Robot` | **required** | The robot to delegate to |
|
|
809
|
-
| `task` | `String` | **required** | The message to send |
|
|
810
|
-
| `async` | `Boolean` | `false` | When true, returns a `DelegationFuture` immediately |
|
|
811
|
-
| `**run_kwargs` | `Hash` | `{}` | Forwarded verbatim to the delegatee's `run` — including `tools:`/`mcp:`, which still default to `:none` |
|
|
812
|
-
|
|
813
|
-
**Synchronous** (default) blocks until the delegatee finishes and returns its
|
|
814
|
-
`RobotResult` with `duration` and `delegated_by` set.
|
|
815
|
-
|
|
816
|
-
**Asynchronous** (`async: true`) runs the delegatee on a new `Thread` and returns
|
|
817
|
-
a `RobotLab::DelegationFuture`. Call `future.value` to block, `future.value(timeout: N)`
|
|
818
|
-
to block with a deadline (raises `RobotLab::DelegationFuture::DelegationTimeout`),
|
|
819
|
-
or `future.resolved?` to poll. An exception in the delegatee is captured and
|
|
820
|
-
re-raised from `future.value`.
|
|
821
|
-
|
|
822
|
-
```ruby
|
|
823
|
-
# Synchronous
|
|
824
|
-
result = manager.delegate(to: analyst, task: "What are the risks?")
|
|
825
|
-
result.reply
|
|
826
|
-
result.delegated_by # => "manager"
|
|
827
|
-
result.duration # => 1.43
|
|
828
|
-
|
|
829
|
-
# Async fan-out
|
|
830
|
-
f1 = manager.delegate(to: summarizer, task: "summarize ...", async: true)
|
|
831
|
-
f2 = manager.delegate(to: analyst, task: "analyze ...", async: true, tools: :inherit)
|
|
832
|
-
summary = f1.value
|
|
833
|
-
analysis = f2.value(timeout: 30)
|
|
834
|
-
```
|
|
835
|
-
|
|
836
|
-
### search_history
|
|
837
|
-
|
|
838
|
-
```ruby
|
|
839
|
-
results = robot.search_history(query, limit: 5)
|
|
840
|
-
# => Array<RobotLab::Robot::HistorySearch::HistoryResult>
|
|
841
|
-
```
|
|
842
|
-
|
|
843
|
-
Rank the robot's own conversation messages against a natural-language query
|
|
844
|
-
using stemmed term-frequency cosine similarity.
|
|
845
|
-
|
|
846
|
-
**Parameters:**
|
|
847
|
-
|
|
848
|
-
| Name | Type | Default | Description |
|
|
849
|
-
|------|------|---------|-------------|
|
|
850
|
-
| `query` | `String` | **required** | Natural-language search query |
|
|
851
|
-
| `limit` | `Integer` | `5` | Maximum results to return |
|
|
852
|
-
|
|
853
|
-
**Returns:** `Array<HistoryResult>` sorted by score descending. `HistoryResult`
|
|
854
|
-
is a `Data` type with members `text`, `role`, `score`, and `index`.
|
|
855
|
-
|
|
856
|
-
Messages shorter than `MIN_SCORE_LENGTH` (20 characters) are skipped, as are
|
|
857
|
-
messages that score zero.
|
|
858
|
-
|
|
859
|
-
**Raises:** `RobotLab::DependencyError` when the optional `classifier` gem is not installed.
|
|
860
|
-
|
|
861
|
-
```ruby
|
|
862
|
-
robot.search_history("quarterly revenue", limit: 3).each do |r|
|
|
863
|
-
puts "[#{r.role}] (#{r.score.round(3)}) #{r.text}"
|
|
864
|
-
end
|
|
865
|
-
```
|
|
866
|
-
|
|
867
|
-
### on
|
|
868
|
-
|
|
869
|
-
```ruby
|
|
870
|
-
robot.on(HandlerClass, context: nil)
|
|
871
|
-
# => the registration
|
|
872
|
-
```
|
|
873
|
-
|
|
874
|
-
Register a hook handler on **this robot's** registry (`robot.hooks`). The robot's
|
|
875
|
-
registry is consulted on every run alongside `RobotLab.hooks` (global) and the
|
|
876
|
-
network's registry, in that order.
|
|
877
|
-
|
|
878
|
-
**Parameters:**
|
|
879
|
-
|
|
880
|
-
| Name | Type | Default | Description |
|
|
881
|
-
|------|------|---------|-------------|
|
|
882
|
-
| `handler_class` | `Class` | **required** | The hook handler class |
|
|
883
|
-
| `context` | `Object`, `nil` | `nil` | Optional per-registration context passed to the handler |
|
|
884
|
-
|
|
885
|
-
!!! note "Task hooks bypass robot registries"
|
|
886
|
-
The `:task` hook family resolves against `[RobotLab.hooks, network&.hooks]`
|
|
887
|
-
only. A handler registered with `robot.on` never fires for task hooks —
|
|
888
|
-
register it with `RobotLab.on` or `network.on` instead.
|
|
889
|
-
|
|
890
|
-
Handlers can also be scoped to a single call with `robot.run(msg, hooks: [HandlerClass])`.
|
|
891
|
-
|
|
892
|
-
### chat_provider
|
|
893
|
-
|
|
894
|
-
```ruby
|
|
895
|
-
robot.chat_provider
|
|
896
|
-
# => String or nil
|
|
897
|
-
```
|
|
898
|
-
|
|
899
|
-
Return the provider for this robot's chat. Useful for displaying model/provider info without reaching into chat internals.
|
|
900
|
-
|
|
901
|
-
### mcp_client
|
|
902
|
-
|
|
903
|
-
```ruby
|
|
904
|
-
robot.mcp_client("github")
|
|
905
|
-
# => MCP::Client or nil
|
|
906
|
-
```
|
|
907
|
-
|
|
908
|
-
Find an MCP client by server name.
|
|
909
|
-
|
|
910
|
-
**Parameters:**
|
|
911
|
-
|
|
912
|
-
| Name | Type | Description |
|
|
913
|
-
|------|------|-------------|
|
|
914
|
-
| `server_name` | `String` | The MCP server name |
|
|
915
|
-
|
|
916
|
-
**Returns:** `MCP::Client` or `nil`
|
|
917
|
-
|
|
918
|
-
### disconnect
|
|
919
|
-
|
|
920
|
-
```ruby
|
|
921
|
-
robot.disconnect
|
|
922
|
-
# => self
|
|
923
|
-
```
|
|
924
|
-
|
|
925
|
-
Disconnect from all MCP servers and bus channels.
|
|
926
|
-
|
|
927
|
-
### to_h
|
|
928
|
-
|
|
929
|
-
```ruby
|
|
930
|
-
robot.to_h
|
|
931
|
-
# => Hash
|
|
932
|
-
```
|
|
933
|
-
|
|
934
|
-
Returns a hash representation of the robot. Keys, in order: `name`,
|
|
935
|
-
`description`, `template`, `skills`, `system_prompt`, `local_tools` (tool names),
|
|
936
|
-
`mcp_tools` (tool names), `mcp_config`, `tools_config`, `mcp_servers` (connected
|
|
937
|
-
client names), `model`, `config` (the `RunConfig` as a JSON-safe hash, omitted
|
|
938
|
-
when the config is empty), and `bus` (`true` if configured, omitted otherwise).
|
|
939
|
-
The whole hash is `.compact`ed, so nil values are dropped.
|
|
940
|
-
|
|
941
|
-
```ruby
|
|
942
|
-
RobotLab.build(name: "x", max_tokens: 100).to_h
|
|
943
|
-
# => { name: "x", local_tools: [], mcp_tools: [], mcp_config: :none,
|
|
944
|
-
# tools_config: :none, mcp_servers: [], model: "claude-sonnet-4-20250514",
|
|
945
|
-
# config: { max_tokens: 100, enable_cache: true } }
|
|
946
|
-
```
|
|
947
|
-
|
|
948
|
-
The `config` value comes from `RunConfig#to_json_hash`, which omits the
|
|
949
|
-
non-serializable fields (`on_tool_call`, `on_tool_result`, `on_content`, `bus`,
|
|
950
|
-
`auto_compact`).
|
|
951
|
-
|
|
952
|
-
## Memory Behavior
|
|
953
|
-
|
|
954
|
-
- **Standalone**: Robot uses its own inherent `Memory` instance (`robot.memory`).
|
|
955
|
-
- **In a Network**: Robot uses the network's shared memory (passed via `network_memory:`).
|
|
956
|
-
|
|
957
|
-
```ruby
|
|
958
|
-
# Standalone memory access
|
|
959
|
-
robot.memory[:user_id] = 123
|
|
960
|
-
robot.memory[:user_id] # => 123
|
|
961
|
-
|
|
962
|
-
# Reset standalone memory
|
|
963
|
-
robot.reset_memory
|
|
964
|
-
```
|
|
965
|
-
|
|
966
|
-
## Templates
|
|
967
|
-
|
|
968
|
-
Templates are `.md` files with optional YAML front matter, loaded via `prompt_manager`. The `template:` parameter maps to a file path relative to the configured template directory:
|
|
969
|
-
|
|
970
|
-
```ruby
|
|
971
|
-
# template: :assistant => prompts/assistant.md
|
|
972
|
-
robot = RobotLab.build(name: "bot", template: :assistant, context: { tone: "friendly" })
|
|
973
|
-
```
|
|
974
|
-
|
|
975
|
-
Front matter supports two categories of keys.
|
|
976
|
-
|
|
977
|
-
**LLM Config:** `model`, `temperature`, `top_p`, `top_k`, `max_tokens`,
|
|
978
|
-
`presence_penalty`, `frequency_penalty`, `stop` are all *parsed* into a
|
|
979
|
-
`RunConfig`.
|
|
980
|
-
|
|
981
|
-
!!! warning "Only `model` and `temperature` actually take effect from front matter"
|
|
982
|
-
Front-matter LLM fields are applied through `RunConfig#apply_to`, which
|
|
983
|
-
dispatches `chat.with_<field>` guarded by `respond_to?`. `RubyLLM::Chat`
|
|
984
|
-
defines only `with_model` and `with_temperature`, so `top_p`, `top_k`,
|
|
985
|
-
`max_tokens`, `presence_penalty`, `frequency_penalty`, and `stop` are parsed
|
|
986
|
-
and **silently dropped**. Set those six as constructor kwargs or on a
|
|
987
|
-
`config:` `RunConfig` instead — that path goes through `with_params` and
|
|
988
|
-
does work.
|
|
989
|
-
|
|
990
|
-
**Robot Extras:** `robot_name`, `description`, `tools`, `mcp`, `skills` — applied to the robot's identity and capabilities. Constructor-provided values always take precedence.
|
|
991
|
-
|
|
992
|
-
| Key | Type | Description |
|
|
993
|
-
|-----|------|-------------|
|
|
994
|
-
| `robot_name` | `String` | Override robot name — applied only when the constructor name is still the default `"robot"` |
|
|
995
|
-
| `description` | `String` | Human-readable description; applied only when the constructor passed no `description:` |
|
|
996
|
-
| `tools` | `Array` | Tool entries; applied only when `local_tools:` is empty (see below) |
|
|
997
|
-
| `mcp` | `Array<Hash>` | MCP server configurations; applied only when the constructor `mcp:` is `:none` |
|
|
998
|
-
| `skills` | `Array<Symbol>` | Skill templates to prepend (recursive, with cycle detection) |
|
|
999
|
-
|
|
1000
|
-
Templates render with ERB — write `<%= var %>`. `{{ var }}` is not interpolated
|
|
1001
|
-
and passes through verbatim.
|
|
1002
|
-
|
|
1003
|
-
### Front-matter `tools:` resolution
|
|
1004
|
-
|
|
1005
|
-
Front-matter `tools` entries are resolved by `resolve_frontmatter_tools`, and the
|
|
1006
|
-
result becomes `local_tools` (real tool objects), **not** the `tools_config`
|
|
1007
|
-
name allowlist. Three entry shapes are accepted:
|
|
1008
|
-
|
|
1009
|
-
| Entry | Behavior |
|
|
1010
|
-
|-------|----------|
|
|
1011
|
-
| `String` | Resolved with `Object.const_get`. If the constant is a `Class` that is `< RubyLLM::Tool`, it is **instantiated** (`const.new`); any other constant is used as-is |
|
|
1012
|
-
| `Class` | **Instantiated** (`name.new`) |
|
|
1013
|
-
| anything else | Used as-is (e.g. an already-built tool instance) |
|
|
1014
|
-
|
|
1015
|
-
An unresolvable name does **not** raise. It is logged at `:warn`
|
|
1016
|
-
(`"Robot '<name>': tool '<X>' not found, skipping"`) and skipped.
|
|
1017
|
-
|
|
1018
|
-
```markdown
|
|
1019
|
-
---
|
|
1020
|
-
tools:
|
|
1021
|
-
- OrderLookup # instantiated: OrderLookup.new
|
|
1022
|
-
- RefundProcessor
|
|
1023
|
-
---
|
|
1024
|
-
```
|
|
1025
|
-
|
|
1026
|
-
Because `run()` still defaults to `tools: :none`, front-matter tools are sent
|
|
1027
|
-
only when you pass `tools: :inherit` at run time.
|
|
1028
|
-
|
|
1029
|
-
## Skills
|
|
1030
|
-
|
|
1031
|
-
Skills compose robot behaviors from reusable templates. Each skill is a standard `.md` template whose prompt body is prepended before the main template. Skills are expanded depth-first with automatic cycle detection.
|
|
1032
|
-
|
|
1033
|
-
**Constructor:** `skills:` accepts `Symbol` or `Array<Symbol>`:
|
|
1034
|
-
|
|
1035
|
-
```ruby
|
|
1036
|
-
robot = RobotLab.build(
|
|
1037
|
-
name: "support",
|
|
1038
|
-
template: :support,
|
|
1039
|
-
skills: [:clarifier, :json_responder]
|
|
1040
|
-
)
|
|
1041
|
-
```
|
|
1042
|
-
|
|
1043
|
-
**Front matter:** templates can declare skills via `skills:` key:
|
|
1044
|
-
|
|
1045
|
-
```markdown
|
|
1046
|
-
---
|
|
1047
|
-
skills:
|
|
1048
|
-
- clarifier
|
|
1049
|
-
- json_responder
|
|
1050
|
-
---
|
|
1051
|
-
Main template body here.
|
|
1052
|
-
```
|
|
1053
|
-
|
|
1054
|
-
Constructor `skills:` and front matter `skills:` are combined (constructor first, then front matter). Skills can nest (a skill can declare its own `skills:` in front matter).
|
|
1055
|
-
|
|
1056
|
-
**Config cascade:** skill config merges in processing order (deepest first). Later values override earlier. Constructor kwargs always win.
|
|
1057
|
-
|
|
1058
|
-
**Prompt order:** skill bodies are concatenated in expansion order, followed by the main template body. All are joined with `"\n\n"` and set as system instructions via a single `with_instructions` call.
|
|
1059
|
-
|
|
1060
|
-
**Cycle detection:** if skills form a cycle, the duplicate is skipped with a logger warning.
|
|
1061
|
-
|
|
1062
|
-
## RunConfig
|
|
1063
|
-
|
|
1064
|
-
`RunConfig` provides shared operational defaults that flow through the configuration hierarchy. Pass it via the `config:` parameter on `Robot.new` or `RobotLab.build`.
|
|
1065
|
-
|
|
1066
|
-
```ruby
|
|
1067
|
-
shared = RobotLab::RunConfig.new(model: "claude-sonnet-4", temperature: 0.7)
|
|
1068
|
-
|
|
1069
|
-
robot = RobotLab.build(
|
|
1070
|
-
name: "writer",
|
|
1071
|
-
system_prompt: "You write creatively.",
|
|
1072
|
-
config: shared,
|
|
1073
|
-
temperature: 0.9 # explicit kwargs override config
|
|
1074
|
-
)
|
|
1075
|
-
|
|
1076
|
-
robot.config #=> RunConfig with model: "claude-sonnet-4", temperature: 0.9, ...
|
|
1077
|
-
```
|
|
1078
|
-
|
|
1079
|
-
`RunConfig::FIELDS` is the complete, authoritative list. Passing any other key to
|
|
1080
|
-
`RunConfig.new` raises `ArgumentError: Unknown RunConfig field: ...`.
|
|
1081
|
-
|
|
1082
|
-
| Group | Constant | Fields |
|
|
1083
|
-
|-------|----------|--------|
|
|
1084
|
-
| LLM | `LLM_FIELDS` | `model`, `temperature`, `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop` |
|
|
1085
|
-
| Tools | `TOOL_FIELDS` | `mcp`, `tools` |
|
|
1086
|
-
| Callbacks | `CALLBACK_FIELDS` | `on_tool_call`, `on_tool_result`, `on_content` |
|
|
1087
|
-
| Infrastructure | `INFRA_FIELDS` | `bus`, `enable_cache`, `max_tool_rounds`, `token_budget`, `cost_budget`, `ractor_pool_size`, `max_concurrent_robots`, `doom_loop_threshold`, `auto_compact`, `compact_threshold`, `max_tools` |
|
|
1088
|
-
|
|
1089
|
-
Five of those infrastructure fields are `RunConfig`-only — they are **not**
|
|
1090
|
-
`Robot.new` keywords: `ractor_pool_size`, `max_concurrent_robots`,
|
|
1091
|
-
`auto_compact`, `compact_threshold`, `max_tools`. (`max_concurrent_robots` is
|
|
1092
|
-
consumed by `Network`, not by `Robot`; `ractor_pool_size` by the
|
|
1093
|
-
`robot_lab-ractor` extension.)
|
|
1094
|
-
|
|
1095
|
-
```ruby
|
|
1096
|
-
config = RobotLab::RunConfig.new(auto_compact: :context_window, compact_threshold: 0.7, max_tools: 32)
|
|
1097
|
-
robot = RobotLab.build(name: "long_runner", system_prompt: "...", config: config)
|
|
1098
|
-
```
|
|
1099
|
-
|
|
1100
|
-
Other `RunConfig` API:
|
|
1101
|
-
|
|
1102
|
-
| Method | Description |
|
|
1103
|
-
|--------|-------------|
|
|
1104
|
-
| `RunConfig.new(**kwargs) { \|c\| ... }` | Keyword construction plus an optional block DSL (`c.model "..."`) |
|
|
1105
|
-
| `#merge(other)` | Returns a **new** RunConfig; the other's non-nil values win |
|
|
1106
|
-
| `#to_h` | The explicitly-set fields |
|
|
1107
|
-
| `#to_json_hash` | `to_h` minus `NON_SERIALIZABLE_FIELDS` (`on_tool_call`, `on_tool_result`, `on_content`, `bus`, `auto_compact`) |
|
|
1108
|
-
| `#apply_to(chat, provider: nil, assume_model_exists: false)` | Applies `LLM_FIELDS` via `chat.with_<field>`, guarded by `respond_to?` |
|
|
1109
|
-
| `#empty?` / `#key?(field)` | Introspection |
|
|
1110
|
-
| `RunConfig.from_front_matter(metadata)` | Builds a RunConfig from a template's parsed metadata |
|
|
1111
|
-
|
|
1112
|
-
!!! note "A network-level `config:` only propagates `mcp` and `tools`"
|
|
1113
|
-
LLM fields and callbacks (`on_content`, `on_tool_call`, `on_tool_result`)
|
|
1114
|
-
are read from the robot's own config at construction time and are never
|
|
1115
|
-
inherited from a network. A member robot picks up the network's `mcp`/`tools`
|
|
1116
|
-
only when it opts in with `:inherit`. `max_concurrent_robots` is the one
|
|
1117
|
-
field the network itself consumes.
|
|
1118
|
-
|
|
1119
|
-
See [Configuration: RunConfig](../../getting-started/configuration.md#runconfig-shared-operational-defaults) for full details.
|
|
1120
|
-
|
|
1121
|
-
## Streaming
|
|
1122
|
-
|
|
1123
|
-
Robots support two complementary approaches for streaming LLM content in real-time.
|
|
1124
|
-
|
|
1125
|
-
### The Chunk Object
|
|
1126
|
-
|
|
1127
|
-
Both callbacks and blocks receive a [`RubyLLM::Chunk`](https://rubyllm.com/streaming/#basic-streaming) (subclass of `RubyLLM::Message`). Key accessors:
|
|
1128
|
-
|
|
1129
|
-
| Accessor | Type | Description |
|
|
1130
|
-
|----------|------|-------------|
|
|
1131
|
-
| `content` | `String`, `nil` | The text delta for this chunk (`nil` on tool-call or usage-only chunks) |
|
|
1132
|
-
| `role` | `Symbol` | Always `:assistant` |
|
|
1133
|
-
| `model_id` | `String` | The LLM model ID |
|
|
1134
|
-
| `tool_calls` | `Array`, `nil` | Tool call deltas (partial JSON arguments) |
|
|
1135
|
-
| `tool_call?` | `Boolean` | Whether this chunk contains tool call data |
|
|
1136
|
-
| `thinking` | `Thinking`, `nil` | Extended thinking delta (Anthropic only) |
|
|
1137
|
-
| `input_tokens` | `Integer`, `nil` | Input token count (populated on final chunk) |
|
|
1138
|
-
| `output_tokens` | `Integer`, `nil` | Output token count (populated on final chunk) |
|
|
1139
|
-
| `cached_tokens` | `Integer`, `nil` | Cached prompt tokens (final chunk) |
|
|
1140
|
-
|
|
1141
|
-
Most chunks carry only `content` (the text delta). The final chunk(s) carry token usage counts. Tool call chunks have `tool_calls` instead of `content`.
|
|
1142
|
-
|
|
1143
|
-
### Stored Callback (`on_content:`)
|
|
1144
|
-
|
|
1145
|
-
Wired at build time via constructor or RunConfig. Fires on every `run()` call automatically:
|
|
1146
|
-
|
|
1147
|
-
```ruby
|
|
1148
|
-
robot = RobotLab.build(
|
|
1149
|
-
name: "assistant",
|
|
1150
|
-
system_prompt: "You are helpful.",
|
|
1151
|
-
on_content: ->(chunk) { broadcast(chunk.content) }
|
|
1152
|
-
)
|
|
1153
|
-
robot.run("Tell me a story") # streams via stored callback
|
|
1154
|
-
```
|
|
1155
|
-
|
|
1156
|
-
The `on_content` callback participates in the RunConfig cascade:
|
|
1157
|
-
|
|
1158
|
-
```ruby
|
|
1159
|
-
config = RobotLab::RunConfig.new(
|
|
1160
|
-
on_content: ->(chunk) { log(chunk.content) }
|
|
1161
|
-
)
|
|
1162
|
-
robot = RobotLab.build(name: "bot", config: config)
|
|
1163
|
-
```
|
|
1164
|
-
|
|
1165
|
-
Constructor `on_content:` overrides RunConfig `on_content`.
|
|
1166
|
-
|
|
1167
|
-
### Per-Call Block
|
|
1168
|
-
|
|
1169
|
-
Pass a block to `run()` for one-off streaming:
|
|
1170
|
-
|
|
1171
|
-
```ruby
|
|
1172
|
-
robot.run("Tell me a story") { |chunk| print chunk.content }
|
|
1173
|
-
```
|
|
1174
|
-
|
|
1175
|
-
### Both Together
|
|
1176
|
-
|
|
1177
|
-
When both exist, both fire — stored callback first, then runtime block:
|
|
1178
|
-
|
|
1179
|
-
```ruby
|
|
1180
|
-
robot = RobotLab.build(
|
|
1181
|
-
name: "bot",
|
|
1182
|
-
system_prompt: "You are helpful.",
|
|
1183
|
-
on_content: ->(chunk) { log(chunk.content) }
|
|
1184
|
-
)
|
|
1185
|
-
robot.run("Tell me a story") { |chunk| stream_to_client(chunk.content) }
|
|
1186
|
-
# log() fires first, then stream_to_client()
|
|
1187
|
-
```
|
|
1188
|
-
|
|
1189
|
-
## Configuration Hierarchy
|
|
1190
|
-
|
|
1191
|
-
Tools and MCP servers use hierarchical resolution: **runtime > robot > task > network > global config**.
|
|
1192
|
-
|
|
1193
|
-
```
|
|
1194
|
-
RobotLab.config (global)
|
|
1195
|
-
|
|
|
1196
|
-
+-- Network (config:) -- propagates only mcp/tools to members
|
|
1197
|
-
| |
|
|
1198
|
-
| +-- Task (config:) -- likewise only mcp/tools
|
|
1199
|
-
| | |
|
|
1200
|
-
| | +-- Robot (config: + build-time mcp:, tools:)
|
|
1201
|
-
| | |
|
|
1202
|
-
| | +-- run() call (runtime mcp:, tools:) <- default :none
|
|
1203
|
-
```
|
|
1204
|
-
|
|
1205
|
-
Values at each level:
|
|
1206
|
-
|
|
1207
|
-
- `:none` -- no tools/MCP at this level (the default at every level)
|
|
1208
|
-
- `:inherit` -- inherit from the parent level
|
|
1209
|
-
- `Array` -- a filter over the already-attached tools, or a list of MCP server configs. Entries are matched against `tool.name.to_s`, so they must be written in the same form the tool was attached in: a class-attached tool matches `"RefundTool"`, an instance-attached one matches `"refund"`. (The constructor's `tools:` accepts only Strings/Symbols; the class form is usable at the task/`run` level, which is not validated.)
|
|
1210
|
-
|
|
1211
|
-
!!! danger "For a standalone robot, do not set `tools: :inherit` at build time"
|
|
1212
|
-
The parent is recomputed on every run as
|
|
1213
|
-
`network_config&.tools || network_parent_config(network)&.tools || RobotLab.config.tools`.
|
|
1214
|
-
For a **standalone** robot that resolves to the global `:none`, so a
|
|
1215
|
-
build-time `:inherit` produces the allowlist `["none"]`, which matches
|
|
1216
|
-
nothing. Leave `tools:` unset on the constructor and pass `tools: :inherit`
|
|
1217
|
-
on `run()` instead.
|
|
1218
|
-
|
|
1219
|
-
| build `tools:` | run `tools:` | tools sent |
|
|
1220
|
-
|---|---|---|
|
|
1221
|
-
| unset | `:none` (default) | none |
|
|
1222
|
-
| unset | `:inherit` | all attached — **the correct pattern** |
|
|
1223
|
-
| `:inherit` | `:inherit` | none — broken |
|
|
1224
|
-
| `:none` | `:inherit` | all attached |
|
|
1225
|
-
|
|
1226
|
-
This does **not** generalize to robots inside a network. When the network's
|
|
1227
|
-
`config:` sets `tools:`/`mcp:`, the parent resolved at run time is that
|
|
1228
|
-
network value, and a build-time `:inherit` is exactly how the robot opts
|
|
1229
|
-
into it. See [MCP in Networks](../mcp/index.md#mcp-in-networks).
|
|
1230
|
-
|
|
1231
|
-
### Per-robot config cascade
|
|
1232
|
-
|
|
1233
|
-
For a single robot, least- to most-specific:
|
|
1234
|
-
|
|
1235
|
-
```
|
|
1236
|
-
template front matter -> config: (RunConfig) -> constructor kwargs
|
|
1237
|
-
```
|
|
1238
|
-
|
|
1239
|
-
Front matter is the **base**, not an override. Constructor kwargs always win.
|
|
1240
|
-
|
|
1241
|
-
## Examples
|
|
1242
|
-
|
|
1243
|
-
### Basic Robot
|
|
1244
|
-
|
|
1245
|
-
```ruby
|
|
1246
|
-
robot = RobotLab.build(
|
|
1247
|
-
name: "greeter",
|
|
1248
|
-
system_prompt: "You greet users warmly."
|
|
1249
|
-
)
|
|
1250
|
-
result = robot.run("Hello!")
|
|
1251
|
-
puts result.last_text_content
|
|
1252
|
-
```
|
|
1253
|
-
|
|
1254
|
-
### Robot with Template
|
|
1255
|
-
|
|
1256
|
-
```ruby
|
|
1257
|
-
robot = RobotLab.build(
|
|
1258
|
-
name: "support",
|
|
1259
|
-
template: :support,
|
|
1260
|
-
context: { company: "Acme Corp" }
|
|
1261
|
-
)
|
|
1262
|
-
result = robot.run("I need help with my order")
|
|
1263
|
-
```
|
|
1264
|
-
|
|
1265
|
-
### Robot with Tools
|
|
1266
|
-
|
|
1267
|
-
```ruby
|
|
1268
|
-
class Calculator < RubyLLM::Tool
|
|
1269
|
-
description "Performs basic arithmetic"
|
|
1270
|
-
param :operation, type: "string", desc: "add, subtract, multiply, divide"
|
|
1271
|
-
param :a, type: "number", desc: "First operand"
|
|
1272
|
-
param :b, type: "number", desc: "Second operand"
|
|
1273
|
-
|
|
1274
|
-
def execute(operation:, a:, b:)
|
|
1275
|
-
case operation
|
|
1276
|
-
when "add" then a + b
|
|
1277
|
-
when "subtract" then a - b
|
|
1278
|
-
when "multiply" then a * b
|
|
1279
|
-
when "divide" then a.to_f / b
|
|
1280
|
-
end
|
|
1281
|
-
end
|
|
1282
|
-
end
|
|
1283
|
-
|
|
1284
|
-
robot = RobotLab.build(
|
|
1285
|
-
name: "math_bot",
|
|
1286
|
-
system_prompt: "You help with math.",
|
|
1287
|
-
local_tools: [Calculator]
|
|
1288
|
-
)
|
|
1289
|
-
|
|
1290
|
-
# run() defaults to tools: :none — pass :inherit to actually send Calculator
|
|
1291
|
-
result = robot.run("What is 15 * 7?", tools: :inherit)
|
|
1292
|
-
```
|
|
1293
|
-
|
|
1294
|
-
Note that `param` accepts only `type:`, `desc:`/`description:`, and `required:` —
|
|
1295
|
-
there is no `enum:` option. See [Tool](tool.md#param).
|
|
1296
|
-
|
|
1297
|
-
### Robot with Local Provider
|
|
1298
|
-
|
|
1299
|
-
```ruby
|
|
1300
|
-
robot = RobotLab.build(
|
|
1301
|
-
name: "local_bot",
|
|
1302
|
-
model: "llama3.2",
|
|
1303
|
-
provider: :ollama,
|
|
1304
|
-
system_prompt: "You are helpful."
|
|
1305
|
-
)
|
|
1306
|
-
result = robot.run("Hello!")
|
|
1307
|
-
```
|
|
1308
|
-
|
|
1309
|
-
`provider:` is threaded through on every re-application of the effective `RunConfig` — including when a template's front matter is re-rendered mid-run — so a local-provider robot (Ollama, GPUStack, LM Studio) doesn't fall back to RubyLLM's static model registry lookup on later turns and raise a spurious "model not found" error.
|
|
1310
|
-
|
|
1311
|
-
Some local/thinking-mode models (e.g. `qwen3` on Ollama) route all of their output through reasoning content rather than the normal response text. When `response.content` is `nil`, `result.reply` falls back first to `response.thinking.text` (RubyLLM's extended-thinking text), then to the most recent assistant text from later in *the current turn only* — never a stale reply left over from a previous turn.
|
|
1312
|
-
|
|
1313
|
-
### Robot with MCP
|
|
1314
|
-
|
|
1315
|
-
```ruby
|
|
1316
|
-
robot = RobotLab.build(
|
|
1317
|
-
name: "developer",
|
|
1318
|
-
system_prompt: "You help with coding tasks.",
|
|
1319
|
-
mcp: [
|
|
1320
|
-
{
|
|
1321
|
-
name: "github",
|
|
1322
|
-
transport: { type: "stdio", command: "github-mcp-server", args: ["stdio"] }
|
|
1323
|
-
}
|
|
1324
|
-
]
|
|
1325
|
-
)
|
|
1326
|
-
|
|
1327
|
-
# mcp: :inherit triggers the connection; tools: :inherit sends the discovered tools
|
|
1328
|
-
result = robot.run("Search for popular Ruby repos", mcp: :inherit, tools: :inherit)
|
|
1329
|
-
robot.disconnect
|
|
1330
|
-
```
|
|
1331
|
-
|
|
1332
|
-
`transport:` must be a nested hash. MCP connection failures are logged and
|
|
1333
|
-
recorded in `robot.failed_mcp_server_names` — they are not raised.
|
|
1334
|
-
|
|
1335
|
-
### Robot with Skills
|
|
1336
|
-
|
|
1337
|
-
```ruby
|
|
1338
|
-
robot = RobotLab.build(
|
|
1339
|
-
name: "support",
|
|
1340
|
-
template: :support,
|
|
1341
|
-
skills: [:clarifier, :safety, :json_responder],
|
|
1342
|
-
context: { company: "Acme Corp" }
|
|
1343
|
-
)
|
|
1344
|
-
result = robot.run("I need help with my order")
|
|
1345
|
-
```
|
|
1346
|
-
|
|
1347
|
-
### Bare Robot with Chaining
|
|
1348
|
-
|
|
1349
|
-
```ruby
|
|
1350
|
-
robot = RobotLab.build(name: "bot")
|
|
1351
|
-
result = robot
|
|
1352
|
-
.with_instructions("Be concise.")
|
|
1353
|
-
.with_temperature(0.3)
|
|
1354
|
-
.run("Explain quantum computing")
|
|
1355
|
-
```
|
|
1356
|
-
|
|
1357
|
-
### Robot with Message Bus
|
|
1358
|
-
|
|
1359
|
-
```ruby
|
|
1360
|
-
bus = TypedBus::MessageBus.new
|
|
1361
|
-
|
|
1362
|
-
bob = RobotLab.build(name: "bob", system_prompt: "You tell jokes.", bus: bus)
|
|
1363
|
-
|
|
1364
|
-
alice = RobotLab.build(name: "alice", system_prompt: "You evaluate jokes.", bus: bus)
|
|
1365
|
-
alice.on_message do |message|
|
|
1366
|
-
verdict = alice.run("Is this funny? #{message.content}").last_text_content
|
|
1367
|
-
puts verdict
|
|
1368
|
-
end
|
|
1369
|
-
|
|
1370
|
-
bob.on_message do |message|
|
|
1371
|
-
joke = bob.run(message.content.to_s).last_text_content
|
|
1372
|
-
bob.send_reply(to: message.from.to_sym, content: joke, in_reply_to: message.key)
|
|
1373
|
-
end
|
|
1374
|
-
|
|
1375
|
-
alice.send_message(to: :bob, content: "Tell me a robot joke.")
|
|
1376
|
-
```
|
|
1377
|
-
|
|
1378
|
-
### Spawning Robots Dynamically
|
|
1379
|
-
|
|
1380
|
-
```ruby
|
|
1381
|
-
# Parent robot spawns specialists on demand
|
|
1382
|
-
dispatcher = RobotLab.build(
|
|
1383
|
-
name: "dispatcher",
|
|
1384
|
-
system_prompt: "You delegate work."
|
|
1385
|
-
)
|
|
1386
|
-
|
|
1387
|
-
dispatcher.on_message do |message|
|
|
1388
|
-
puts "Reply from #{message.from}: #{message.content}"
|
|
1389
|
-
end
|
|
1390
|
-
|
|
1391
|
-
# spawn creates child on same bus (bus created lazily)
|
|
1392
|
-
helper = dispatcher.spawn(
|
|
1393
|
-
name: "helper",
|
|
1394
|
-
system_prompt: "You answer questions concisely."
|
|
1395
|
-
)
|
|
1396
|
-
|
|
1397
|
-
answer = helper.run("What is 2+2?").last_text_content
|
|
1398
|
-
helper.send_message(to: :dispatcher, content: answer)
|
|
1399
|
-
```
|
|
1400
|
-
|
|
1401
|
-
### Connecting to a Bus After Creation
|
|
1402
|
-
|
|
1403
|
-
```ruby
|
|
1404
|
-
bot = RobotLab.build(name: "latecomer", system_prompt: "Hi there.")
|
|
1405
|
-
|
|
1406
|
-
# Join a bus later
|
|
1407
|
-
bus = TypedBus::MessageBus.new
|
|
1408
|
-
bot.with_bus(bus)
|
|
1409
|
-
|
|
1410
|
-
# Now bot can send/receive messages
|
|
1411
|
-
bot.send_message(to: :someone, content: "Hello!")
|
|
1412
|
-
```
|
|
1413
|
-
|
|
1414
|
-
## Token & Cost Tracking
|
|
1415
|
-
|
|
1416
|
-
Every `robot.run()` returns a `RobotResult` with token counts for that call. The robot accumulates running totals across all runs.
|
|
1417
|
-
|
|
1418
|
-
### RobotResult Token Fields
|
|
1419
|
-
|
|
1420
|
-
| Field | Type | Description |
|
|
1421
|
-
|-------|------|-------------|
|
|
1422
|
-
| `input_tokens` | `Integer` | Input tokens sent to the LLM in this run (0 if provider doesn't report usage) |
|
|
1423
|
-
| `output_tokens` | `Integer` | Output tokens received from the LLM in this run (0 if not reported) |
|
|
1424
|
-
|
|
1425
|
-
### Robot Cumulative Totals
|
|
1426
|
-
|
|
1427
|
-
| Attribute | Type | Description |
|
|
1428
|
-
|-----------|------|-------------|
|
|
1429
|
-
| `total_input_tokens` | `Integer` | Cumulative input tokens across all `run()` calls |
|
|
1430
|
-
| `total_output_tokens` | `Integer` | Cumulative output tokens across all `run()` calls |
|
|
1431
|
-
|
|
1432
|
-
### reset_token_totals
|
|
1433
|
-
|
|
1434
|
-
```ruby
|
|
1435
|
-
robot.reset_token_totals
|
|
1436
|
-
# => the robot itself (returns self, so it chains)
|
|
1437
|
-
```
|
|
1438
|
-
|
|
1439
|
-
Reset the cumulative accounting counters to zero. Useful when you want to measure cost for a specific task batch while keeping the robot alive for the next batch.
|
|
1440
|
-
|
|
1441
|
-
> **Note:** This resets the *accounting counter only* — the underlying chat history keeps growing. The next run's `input_tokens` will reflect the full accumulated chat context sent to the API.
|
|
1442
|
-
|
|
1443
|
-
**Example:**
|
|
1444
|
-
|
|
1445
|
-
```ruby
|
|
1446
|
-
robot = RobotLab.build(name: "analyst", system_prompt: "You are helpful.")
|
|
1447
|
-
|
|
1448
|
-
result = robot.run("What is a stack?")
|
|
1449
|
-
puts result.input_tokens # e.g. 120
|
|
1450
|
-
puts result.output_tokens # e.g. 45
|
|
1451
|
-
|
|
1452
|
-
result2 = robot.run("And a queue?")
|
|
1453
|
-
puts result2.input_tokens # larger — full chat history sent
|
|
1454
|
-
|
|
1455
|
-
puts robot.total_input_tokens # 120 + result2.input_tokens
|
|
1456
|
-
puts robot.total_output_tokens
|
|
1457
|
-
|
|
1458
|
-
# Start a fresh accounting batch
|
|
1459
|
-
robot.reset_token_totals
|
|
1460
|
-
puts robot.total_input_tokens # => 0
|
|
1461
|
-
```
|
|
1462
|
-
|
|
1463
|
-
### Budgets
|
|
1464
|
-
|
|
1465
|
-
`token_budget:` and `cost_budget:` turn the counters above into enforceable ceilings, backed by a thread-safe `RobotLab::Budget::Ledger` (`robot.budget_ledger`, `nil` when neither is configured):
|
|
1466
|
-
|
|
1467
|
-
```ruby
|
|
1468
|
-
robot = RobotLab.build(
|
|
1469
|
-
name: "capped",
|
|
1470
|
-
system_prompt: "...",
|
|
1471
|
-
token_budget: 10_000,
|
|
1472
|
-
cost_budget: 0.50
|
|
1473
|
-
)
|
|
1474
|
-
```
|
|
1475
|
-
|
|
1476
|
-
Each `run()` reserves the remaining budget for every configured dimension before the LLM call, and reconciles the reservation with actual usage after:
|
|
1477
|
-
|
|
1478
|
-
- **`RobotLab::BudgetExceeded`** — raised up front when a *prior* call already exhausted a dimension; the new call is refused before it spends anything.
|
|
1479
|
-
- **`RobotLab::InferenceError`** — raised after the call when *this* call's actual usage (from `RobotResult#input_tokens`/`output_tokens`, and the response's reported cost when the provider supports pricing) pushes cumulative usage over budget. This is the same error `token_budget` alone has always raised; `cost_budget` uses the analogous message (`"Cost budget exceeded: $X used, budget is $Y"`).
|
|
1480
|
-
|
|
1481
|
-
See [Budgets](../../guides/observability.md#budgets-token-cost) for the full walkthrough.
|
|
1482
|
-
|
|
1483
|
-
## Tool Loop Circuit Breaker
|
|
1484
|
-
|
|
1485
|
-
Set `max_tool_rounds:` to guard against a robot looping indefinitely through tool calls. After the limit is reached, `RobotLab::ToolLoopError` is raised.
|
|
1486
|
-
|
|
1487
|
-
### max_tool_rounds Parameter
|
|
1488
|
-
|
|
1489
|
-
```ruby
|
|
1490
|
-
robot = RobotLab.build(
|
|
1491
|
-
name: "runner",
|
|
1492
|
-
system_prompt: "Execute every step.",
|
|
1493
|
-
local_tools: [StepTool],
|
|
1494
|
-
max_tool_rounds: 10
|
|
1495
|
-
)
|
|
1496
|
-
```
|
|
1497
|
-
|
|
1498
|
-
`max_tool_rounds` can also be set via `RunConfig`:
|
|
1499
|
-
|
|
1500
|
-
```ruby
|
|
1501
|
-
config = RobotLab::RunConfig.new(max_tool_rounds: 10)
|
|
1502
|
-
robot = RobotLab.build(name: "runner", system_prompt: "...", config: config)
|
|
1503
|
-
```
|
|
1504
|
-
|
|
1505
|
-
### ToolLoopError
|
|
1506
|
-
|
|
1507
|
-
`RobotLab::ToolLoopError < RobotLab::InferenceError`
|
|
1508
|
-
|
|
1509
|
-
Raised when the number of tool calls in a single `run()` exceeds `max_tool_rounds`. The message reads:
|
|
1510
|
-
|
|
1511
|
-
```
|
|
1512
|
-
Circuit breaker triggered: <N> tool calls exceeded max_tool_rounds (<M>)
|
|
1513
|
-
```
|
|
1514
|
-
|
|
1515
|
-
where `N` is the call count that tripped the breaker and `M` is the configured limit.
|
|
1516
|
-
|
|
1517
|
-
### Recovery after ToolLoopError
|
|
1518
|
-
|
|
1519
|
-
After a `ToolLoopError`, the chat contains a dangling `tool_use` block with no matching `tool_result`. Anthropic and most providers will reject any subsequent request with that broken history.
|
|
1520
|
-
|
|
1521
|
-
**You must call `clear_messages` before reusing the robot:**
|
|
1522
|
-
|
|
1523
|
-
```ruby
|
|
1524
|
-
begin
|
|
1525
|
-
robot.run("Execute all steps.")
|
|
1526
|
-
rescue RobotLab::ToolLoopError => e
|
|
1527
|
-
# "Circuit breaker triggered: 11 tool calls exceeded max_tool_rounds (10)"
|
|
1528
|
-
puts "Circuit breaker fired: #{e.message}"
|
|
1529
|
-
end
|
|
1530
|
-
|
|
1531
|
-
# Flush the corrupted chat (system prompt is kept)
|
|
1532
|
-
robot.clear_messages
|
|
1533
|
-
puts robot.config.max_tool_rounds # still set — config unchanged
|
|
1534
|
-
|
|
1535
|
-
# Robot is healthy again
|
|
1536
|
-
result = robot.run("Something new.")
|
|
1537
|
-
```
|
|
1538
|
-
|
|
1539
|
-
## Doom Loop Detection
|
|
1540
|
-
|
|
1541
|
-
Distinct from the circuit breaker, doom-loop detection is **always on**. Every
|
|
1542
|
-
`run()` unconditionally installs a `RobotLab::DoomLoopDetector` over the chat's
|
|
1543
|
-
`execute_tool`, and removes it again when the run ends. `doom_loop_threshold:`
|
|
1544
|
-
only *tunes* it; it cannot be disabled from the constructor.
|
|
1545
|
-
|
|
1546
|
-
```ruby
|
|
1547
|
-
robot = RobotLab.build(name: "worker", system_prompt: "...", doom_loop_threshold: 5)
|
|
1548
|
-
```
|
|
1549
|
-
|
|
1550
|
-
| | |
|
|
1551
|
-
|---|---|
|
|
1552
|
-
| Default threshold | `RobotLab::DoomLoopDetector::DEFAULT_THRESHOLD` (3) |
|
|
1553
|
-
| Set via | `doom_loop_threshold:` constructor kwarg or `RunConfig#doom_loop_threshold` |
|
|
1554
|
-
|
|
1555
|
-
When a consecutive or cyclic repetition of the same tool name exceeds the
|
|
1556
|
-
threshold, the detector does **not** raise. It appends a self-correction warning
|
|
1557
|
-
to that tool's result so the model can change strategy: a `String` result gets
|
|
1558
|
-
`"\n\n⚠️ <warning>"` appended, and a `Hash` result gains a `:_doom_loop_warning`
|
|
1559
|
-
key. The detector then resets.
|
|
1560
|
-
|
|
1561
|
-
## Learning Accumulation
|
|
1562
|
-
|
|
1563
|
-
`robot.learn(text)` records a cross-run observation. On each subsequent `run()`,
|
|
1564
|
-
**all** accumulated learnings are prepended to the user message as a
|
|
1565
|
-
`LEARNINGS FROM PREVIOUS RUNS:` block. There is no active/inactive distinction —
|
|
1566
|
-
every entry in `robot.learnings` is injected.
|
|
1567
|
-
|
|
1568
|
-
### learn
|
|
1569
|
-
|
|
1570
|
-
```ruby
|
|
1571
|
-
robot.learn(text)
|
|
1572
|
-
# => self
|
|
1573
|
-
```
|
|
1574
|
-
|
|
1575
|
-
Add a learning to the robot's accumulated observations. Learnings are automatically deduplicated:
|
|
1576
|
-
|
|
1577
|
-
- If the new text is a substring of an existing learning, it is dropped (the existing broader learning already covers it).
|
|
1578
|
-
- If an existing learning is a substring of the new text, the narrower one is replaced.
|
|
1579
|
-
|
|
1580
|
-
Learnings are written to the robot's own memory under `memory[:learnings]`.
|
|
1581
|
-
|
|
1582
|
-
!!! note "Learnings do not survive process restart on their own"
|
|
1583
|
-
`initialize_memory` always constructs a fresh `Memory.new`, and there is no
|
|
1584
|
-
`memory:` constructor keyword, so a newly built robot starts with an empty
|
|
1585
|
-
`:learnings` key. `learn` reads back whatever is already in `memory[:learnings]`
|
|
1586
|
-
at construction, which means persistence requires an external store — for
|
|
1587
|
-
example the `robot_lab-durable` extension — to repopulate it.
|
|
1588
|
-
|
|
1589
|
-
`learn` runs inside the `:learn` hook family (`before_learn` / `around_learn` /
|
|
1590
|
-
`after_learn`, plus `on_learn`), so a hook handler can observe or veto the write.
|
|
1591
|
-
|
|
1592
|
-
**Parameters:**
|
|
1593
|
-
|
|
1594
|
-
| Name | Type | Description |
|
|
1595
|
-
|------|------|-------------|
|
|
1596
|
-
| `text` | `String` | The observation or insight to record |
|
|
1597
|
-
|
|
1598
|
-
**Returns:** `self`
|
|
1599
|
-
|
|
1600
|
-
### learnings
|
|
1601
|
-
|
|
1602
|
-
```ruby
|
|
1603
|
-
robot.learnings
|
|
1604
|
-
# => Array<String>
|
|
1605
|
-
```
|
|
1606
|
-
|
|
1607
|
-
Returns the list of accumulated learning strings in insertion order.
|
|
1608
|
-
|
|
1609
|
-
### How Learnings Are Injected
|
|
1610
|
-
|
|
1611
|
-
When learnings are present, each `run(message)` prepends them to the message before sending to the LLM:
|
|
1612
|
-
|
|
1613
|
-
```
|
|
1614
|
-
LEARNINGS FROM PREVIOUS RUNS:
|
|
1615
|
-
- This codebase prefers map/collect over manual array accumulation
|
|
1616
|
-
- Explicit nil comparisons appear frequently here
|
|
1617
|
-
|
|
1618
|
-
<original user message>
|
|
1619
|
-
```
|
|
1620
|
-
|
|
1621
|
-
**Example:**
|
|
1622
|
-
|
|
1623
|
-
```ruby
|
|
1624
|
-
reviewer = RobotLab.build(
|
|
1625
|
-
name: "reviewer",
|
|
1626
|
-
system_prompt: "You are a Ruby code reviewer."
|
|
1627
|
-
)
|
|
1628
|
-
|
|
1629
|
-
# Run 1 — no learnings yet
|
|
1630
|
-
reviewer.run("Review snippet A")
|
|
1631
|
-
reviewer.learn("Prefer map/collect over manual accumulation")
|
|
1632
|
-
|
|
1633
|
-
# Run 2 — learning injected automatically
|
|
1634
|
-
reviewer.run("Review snippet B")
|
|
1635
|
-
reviewer.learn("Avoid explicit nil comparisons")
|
|
1636
|
-
|
|
1637
|
-
# Run 3 — both learnings injected
|
|
1638
|
-
reviewer.run("Review snippet C")
|
|
1639
|
-
|
|
1640
|
-
puts reviewer.learnings.size # => 2
|
|
1641
|
-
```
|
|
1642
|
-
|
|
1643
|
-
### Deduplication Example
|
|
1644
|
-
|
|
1645
|
-
```ruby
|
|
1646
|
-
robot.learn("avoid using puts")
|
|
1647
|
-
robot.learn("avoid using puts and p in production code")
|
|
1648
|
-
# => broader learning replaces narrower; robot.learnings.size == 1
|
|
1649
|
-
```
|
|
1650
|
-
|
|
1651
|
-
## Runnable Protocol
|
|
1652
|
-
|
|
1653
|
-
`Robot` includes `RobotLab::Runnable`, the shared interface it has in common with `Network` — see [Runnable Protocol](../../architecture/core-concepts.md#runnable-protocol) for the full picture. For a single robot:
|
|
1654
|
-
|
|
1655
|
-
| Method | Returns |
|
|
1656
|
-
|--------|---------|
|
|
1657
|
-
| `crew` | `[self]` — a robot is a crew of one |
|
|
1658
|
-
| `chief` | `self` |
|
|
1659
|
-
| `robot_count` | `1` |
|
|
1660
|
-
| `network?` | `false` |
|
|
1661
|
-
| `single?` | `true` |
|
|
1662
|
-
|
|
1663
|
-
## See Also
|
|
1664
|
-
|
|
1665
|
-
- [Building Robots Guide](../../guides/building-robots.md) (includes [Composable Skills](../../guides/building-robots.md#composable-skills))
|
|
1666
|
-
- [Tool](tool.md)
|
|
1667
|
-
- [Network](network.md)
|