robot_lab 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +1 -1
- data/lib/robot_lab/mcp/transports/streamable_http.rb +1 -1
- data/lib/robot_lab/version.rb +1 -1
- metadata +3 -253
- data/.architecture/AGENTS.md +0 -32
- data/.architecture/config.yml +0 -8
- data/.architecture/members.yml +0 -60
- data/.architecture/reviews/feature-free-will.md +0 -490
- data/.architecture/reviews/overall-codebase.md +0 -427
- data/.claude/settings.local.json +0 -57
- data/.codex/config.toml +0 -2
- data/.envrc +0 -6
- data/.github/workflows/deploy-github-pages.yml +0 -52
- data/.irbrc +0 -6
- data/.loki +0 -65
- data/.quality/reek_baseline.txt +0 -43
- data/.rubocop.yml +0 -10
- data/Archspec.rb +0 -44
- data/CLAUDE.md +0 -141
- data/COMMITS.md +0 -196
- data/Rakefile +0 -193
- data/_typos.toml +0 -21
- data/agent2agent_review.md +0 -192
- data/agentf_improvements.md +0 -253
- data/agents.md +0 -14
- data/docs/api/core/index.md +0 -147
- data/docs/api/core/memory.md +0 -578
- data/docs/api/core/network.md +0 -553
- data/docs/api/core/result.md +0 -211
- data/docs/api/core/robot.md +0 -1667
- data/docs/api/core/state.md +0 -145
- data/docs/api/core/tool.md +0 -520
- data/docs/api/errors.md +0 -323
- data/docs/api/hooks.md +0 -469
- data/docs/api/index.md +0 -137
- data/docs/api/mcp/client.md +0 -387
- data/docs/api/mcp/index.md +0 -267
- data/docs/api/mcp/server.md +0 -252
- data/docs/api/mcp/transports.md +0 -339
- data/docs/api/messages/index.md +0 -126
- data/docs/api/messages/text-message.md +0 -170
- data/docs/api/messages/tool-call-message.md +0 -206
- data/docs/api/messages/tool-result-message.md +0 -242
- data/docs/api/messages/user-message.md +0 -232
- data/docs/api/skills.md +0 -423
- data/docs/api/streaming/context.md +0 -280
- data/docs/api/streaming/events.md +0 -185
- data/docs/api/streaming/index.md +0 -156
- data/docs/api/support.md +0 -846
- data/docs/architecture/core-concepts.md +0 -575
- data/docs/architecture/index.md +0 -252
- data/docs/architecture/message-flow.md +0 -366
- data/docs/architecture/network-orchestration.md +0 -478
- data/docs/architecture/robot-execution.md +0 -396
- data/docs/architecture/state-management.md +0 -410
- data/docs/assets/css/custom.css +0 -56
- data/docs/assets/images/robot_lab.jpg +0 -0
- data/docs/concepts.md +0 -641
- data/docs/examples/basic-chat.md +0 -258
- data/docs/examples/index.md +0 -425
- data/docs/examples/mcp-server.md +0 -403
- data/docs/examples/multi-robot-network.md +0 -476
- data/docs/examples/tool-usage.md +0 -468
- data/docs/getting-started/configuration.md +0 -645
- data/docs/getting-started/index.md +0 -56
- data/docs/getting-started/installation.md +0 -197
- data/docs/getting-started/quick-start.md +0 -293
- data/docs/guides/building-robots.md +0 -1187
- data/docs/guides/creating-networks.md +0 -736
- data/docs/guides/hooks.md +0 -1206
- data/docs/guides/index.md +0 -78
- data/docs/guides/knowledge.md +0 -219
- data/docs/guides/mcp-integration.md +0 -740
- data/docs/guides/memory.md +0 -458
- data/docs/guides/observability.md +0 -733
- data/docs/guides/streaming.md +0 -323
- data/docs/guides/using-tools.md +0 -780
- data/docs/index.md +0 -280
- data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
- data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
- data/examples/.envrc +0 -3
- data/examples/01_simple_robot.rb +0 -31
- data/examples/02_tools.rb +0 -109
- data/examples/03_network.rb +0 -118
- data/examples/04_mcp.rb +0 -217
- data/examples/05_streaming.rb +0 -137
- data/examples/06_prompt_templates.rb +0 -325
- data/examples/07_network_memory.rb +0 -313
- data/examples/08_llm_config.rb +0 -162
- data/examples/09_chaining.rb +0 -251
- data/examples/10_memory.rb +0 -315
- data/examples/11_network_introspection.rb +0 -239
- data/examples/12_message_bus.rb +0 -67
- data/examples/13_spawn.rb +0 -85
- data/examples/14_rusty_circuit/.envrc +0 -1
- data/examples/14_rusty_circuit/.gitignore +0 -1
- data/examples/14_rusty_circuit/README.md +0 -1
- data/examples/14_rusty_circuit/comic.rb +0 -149
- data/examples/14_rusty_circuit/display.rb +0 -217
- data/examples/14_rusty_circuit/heckler.rb +0 -65
- data/examples/14_rusty_circuit/open_mic.rb +0 -132
- data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
- data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
- data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
- data/examples/14_rusty_circuit/scout.rb +0 -165
- data/examples/14_rusty_circuit/show.log +0 -234
- data/examples/15_memory_network_and_bus/.envrc +0 -1
- data/examples/15_memory_network_and_bus/README.md +0 -66
- data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
- data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
- data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
- data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
- data/examples/15_memory_network_and_bus/output/combined_article.md +0 -11
- data/examples/15_memory_network_and_bus/output/final_article.md +0 -8
- data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
- data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
- data/examples/15_memory_network_and_bus/output/memory.json +0 -13
- data/examples/15_memory_network_and_bus/output/revision_1.md +0 -29
- data/examples/15_memory_network_and_bus/output/revision_2.md +0 -50
- data/examples/15_memory_network_and_bus/output/revision_3.md +0 -8
- data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
- data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
- data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
- data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
- data/examples/16_writers_room/.envrc +0 -1
- data/examples/16_writers_room/display.rb +0 -158
- data/examples/16_writers_room/output/.gitignore +0 -4
- data/examples/16_writers_room/output/README.md +0 -69
- data/examples/16_writers_room/output/opus_001.md +0 -263
- data/examples/16_writers_room/output/opus_001_notes.log +0 -470
- data/examples/16_writers_room/output/opus_002.md +0 -245
- data/examples/16_writers_room/output/opus_002_notes.log +0 -546
- data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
- data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
- data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
- data/examples/16_writers_room/prompts/writer.md +0 -37
- data/examples/16_writers_room/room.rb +0 -186
- data/examples/16_writers_room/tools.rb +0 -173
- data/examples/16_writers_room/writer.rb +0 -121
- data/examples/16_writers_room/writers_room.rb +0 -256
- data/examples/17_skills.rb +0 -234
- data/examples/18_rails/.envrc +0 -3
- data/examples/18_rails/.gitignore +0 -5
- data/examples/18_rails/Gemfile +0 -11
- data/examples/18_rails/README.md +0 -67
- data/examples/18_rails/Rakefile +0 -4
- data/examples/18_rails/app/controllers/application_controller.rb +0 -4
- data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
- data/examples/18_rails/app/jobs/application_job.rb +0 -4
- data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
- data/examples/18_rails/app/models/application_record.rb +0 -5
- data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
- data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
- data/examples/18_rails/app/robots/chat_robot.rb +0 -25
- data/examples/18_rails/app/tools/time_tool.rb +0 -9
- data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
- data/examples/18_rails/app/views/chat/index.html.erb +0 -67
- data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
- data/examples/18_rails/bin/dev +0 -7
- data/examples/18_rails/bin/rails +0 -6
- data/examples/18_rails/bin/setup +0 -15
- data/examples/18_rails/config/application.rb +0 -33
- data/examples/18_rails/config/cable.yml +0 -2
- data/examples/18_rails/config/database.yml +0 -5
- data/examples/18_rails/config/environment.rb +0 -4
- data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
- data/examples/18_rails/config/routes.rb +0 -6
- data/examples/18_rails/config.ru +0 -4
- data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
- data/examples/19_token_tracking.rb +0 -138
- data/examples/20_circuit_breaker.rb +0 -147
- data/examples/21_learning_loop.rb +0 -181
- data/examples/22_context_compression.rb +0 -172
- data/examples/23_convergence.rb +0 -133
- data/examples/24_structured_delegation.rb +0 -151
- data/examples/25_history_search/conversation.jsonl +0 -30
- data/examples/25_history_search.rb +0 -133
- data/examples/26_document_store/api_versioning_adr.md +0 -52
- data/examples/26_document_store/incident_postmortem.md +0 -46
- data/examples/26_document_store/postgres_runbook.md +0 -49
- data/examples/26_document_store/redis_caching_guide.md +0 -48
- data/examples/26_document_store/sidekiq_guide.md +0 -51
- data/examples/26_document_store.rb +0 -140
- data/examples/27_incident_response/README.md +0 -65
- data/examples/27_incident_response/incident_response.rb +0 -261
- data/examples/28_mcp_discovery.rb +0 -113
- data/examples/29_ractor_tools.rb +0 -240
- data/examples/30_ractor_network.rb +0 -252
- data/examples/31_launch_assessment.rb +0 -246
- data/examples/32_newsletter_reader.rb +0 -193
- data/examples/33_stock_generator.rb +0 -80
- data/examples/33_stock_predictor.rb +0 -327
- data/examples/34_agentskills.rb +0 -76
- data/examples/35_hooks.rb +0 -266
- data/examples/README.md +0 -550
- data/examples/common.rb +0 -180
- data/examples/prompts/assistant.md +0 -5
- data/examples/prompts/audit_trail.md +0 -8
- data/examples/prompts/billing.md +0 -10
- data/examples/prompts/classifier.md +0 -7
- data/examples/prompts/comedian.md +0 -6
- data/examples/prompts/comedy_critic.md +0 -10
- data/examples/prompts/configurable.md +0 -9
- data/examples/prompts/dispatcher.md +0 -12
- data/examples/prompts/entity_extractor.md +0 -14
- data/examples/prompts/escalation.md +0 -42
- data/examples/prompts/frontmatter_mcp_test.md +0 -9
- data/examples/prompts/frontmatter_named_test.md +0 -5
- data/examples/prompts/frontmatter_tools_test.md +0 -6
- data/examples/prompts/general.md +0 -7
- data/examples/prompts/github_assistant.md +0 -14
- data/examples/prompts/helper.md +0 -4
- data/examples/prompts/incident_responder.md +0 -18
- data/examples/prompts/keyword_extractor.md +0 -11
- data/examples/prompts/llm_config_demo.md +0 -20
- data/examples/prompts/order_support.md +0 -35
- data/examples/prompts/os_advocate.md +0 -13
- data/examples/prompts/os_chief.md +0 -13
- data/examples/prompts/os_editor.md +0 -13
- data/examples/prompts/parameterized_main_test.md +0 -6
- data/examples/prompts/pii_redactor.md +0 -7
- data/examples/prompts/product_support.md +0 -37
- data/examples/prompts/runbook_protocol.md +0 -12
- data/examples/prompts/sentiment_analyzer.md +0 -12
- data/examples/prompts/skill_a_test.md +0 -4
- data/examples/prompts/skill_b_test.md +0 -4
- data/examples/prompts/skill_config_test.md +0 -5
- data/examples/prompts/skill_cycle_a_test.md +0 -6
- data/examples/prompts/skill_cycle_b_test.md +0 -6
- data/examples/prompts/skill_description_test.md +0 -4
- data/examples/prompts/skill_leaf_test.md +0 -4
- data/examples/prompts/skill_nested_test.md +0 -6
- data/examples/prompts/skill_refs_main_test.md +0 -6
- data/examples/prompts/skill_self_ref_test.md +0 -6
- data/examples/prompts/skill_with_mcp_test.md +0 -9
- data/examples/prompts/skill_with_params_test.md +0 -6
- data/examples/prompts/skill_with_robot_name_test.md +0 -5
- data/examples/prompts/skill_with_tools_test.md +0 -6
- data/examples/prompts/sre_compliance.md +0 -10
- data/examples/prompts/structured_output.md +0 -7
- data/examples/prompts/synthesizer.md +0 -17
- data/examples/prompts/technical.md +0 -10
- data/examples/prompts/template_with_skills_test.md +0 -6
- data/examples/prompts/triage.md +0 -22
- data/examples/ruboruby.md +0 -423
- data/examples/run_all.rb +0 -60
- data/examples/xyzzy.rb +0 -97
- data/logfile +0 -8
- data/mkdocs.yml +0 -209
- data/robot_concurrency.md +0 -38
- data/simple_acp_review.md +0 -298
- data/temp.md +0 -6
- data/tool_manifest_plan.md +0 -155
|
@@ -1,396 +0,0 @@
|
|
|
1
|
-
# Robot Execution
|
|
2
|
-
|
|
3
|
-
This page details how a robot processes messages and generates responses.
|
|
4
|
-
|
|
5
|
-
## Execution Overview
|
|
6
|
-
|
|
7
|
-
When you call `robot.run("message")`, several steps occur:
|
|
8
|
-
|
|
9
|
-
```mermaid
|
|
10
|
-
sequenceDiagram
|
|
11
|
-
participant App as Application
|
|
12
|
-
participant Skills as AgentSkillMatching<br/>(prepended)
|
|
13
|
-
participant Robot as Robot (Hooking run)
|
|
14
|
-
participant Hooks as Hooks / HookRegistry
|
|
15
|
-
participant Memory
|
|
16
|
-
participant Ledger as Budget::Ledger
|
|
17
|
-
participant Chat as @chat (RubyLLM)
|
|
18
|
-
participant LLM
|
|
19
|
-
|
|
20
|
-
App->>Skills: run("message")
|
|
21
|
-
Skills->>Skills: match_agent_skills + inject (prompt & script tools)
|
|
22
|
-
Skills->>Robot: super
|
|
23
|
-
|
|
24
|
-
Robot->>Memory: resolve_run_memory()
|
|
25
|
-
Robot->>Memory: current_writer = name
|
|
26
|
-
Robot->>Hooks: Hooks.run(:run, RunHookContext)
|
|
27
|
-
Note over Hooks: before_run / around_run wrap everything below
|
|
28
|
-
|
|
29
|
-
Robot->>Robot: prepare_tools() — resolve_mcp_hierarchy,<br/>ensure_mcp_clients, resolve_tools_hierarchy,<br/>filtered_tools + cap_tools
|
|
30
|
-
Robot->>Chat: with_tools(*filtered, replace: true)
|
|
31
|
-
Robot->>Robot: rerender_template(kwargs) — when template + extra kwargs
|
|
32
|
-
Robot->>Ledger: reserve_budget! (raises BudgetExceeded if exhausted)
|
|
33
|
-
|
|
34
|
-
Robot->>Hooks: Hooks.run(:llm_generation, LlmGenerationHookContext)
|
|
35
|
-
Robot->>Robot: inject_learnings(request)
|
|
36
|
-
Robot->>Robot: maybe_compact — fires :compaction family
|
|
37
|
-
Robot->>Chat: install_circuit_breaker (when max_tool_rounds)
|
|
38
|
-
Robot->>Chat: install_doom_loop_detection (always)
|
|
39
|
-
Robot->>Chat: ask(message, &streaming_block)
|
|
40
|
-
Chat->>LLM: API Request
|
|
41
|
-
|
|
42
|
-
loop Tool loop (inside :llm_generation)
|
|
43
|
-
LLM-->>Chat: tool_call response
|
|
44
|
-
Chat->>Hooks: Tool.call — Hooks.run(:tool_call)
|
|
45
|
-
Chat->>LLM: tool result
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
LLM-->>Chat: final response
|
|
49
|
-
Chat-->>Robot: RubyLLM::Response
|
|
50
|
-
Robot->>Robot: build_result(response) — result_text + token accounting
|
|
51
|
-
Robot->>Ledger: reconcile_budget!
|
|
52
|
-
Robot->>Robot: enforce_token_budget! / enforce_cost_budget!<br/>(raise InferenceError on overage)
|
|
53
|
-
Robot->>Hooks: after_run
|
|
54
|
-
Note over Robot,Memory: ensure: remove_doom_loop_detection,<br/>restore_tool_call_callback, restore current_writer
|
|
55
|
-
Robot-->>Skills: RobotResult
|
|
56
|
-
Skills->>Skills: ensure: restore_after_agent_skills
|
|
57
|
-
Skills-->>App: RobotResult
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
The steps are described individually below. Two things this diagram makes
|
|
61
|
-
explicit that the prose repeats: `Robot::AgentSkillMatching` is **prepended**, so
|
|
62
|
-
its `run` wraps `Robot::Hooking#run` (which is the `run` everything else calls);
|
|
63
|
-
and the `:llm_generation` family wraps the provider's *entire* tool loop, so
|
|
64
|
-
`:tool_call` hooks fire nested inside it rather than between generations.
|
|
65
|
-
|
|
66
|
-
## Step-by-Step Flow
|
|
67
|
-
|
|
68
|
-
### 1. Memory Resolution
|
|
69
|
-
|
|
70
|
-
`Robot#run` first resolves the memory this run will write to, via `resolve_run_memory`:
|
|
71
|
-
|
|
72
|
-
```ruby
|
|
73
|
-
# resolve_active_memory priority order:
|
|
74
|
-
# 1. Explicit network_memory: parameter
|
|
75
|
-
# 2. Network's memory (if running in a network)
|
|
76
|
-
# 3. Robot's inherent @memory (standalone mode)
|
|
77
|
-
run_memory = resolve_active_memory(network: network, network_memory: network_memory)
|
|
78
|
-
|
|
79
|
-
# A runtime memory: kwarg either replaces (Memory) or merges into (Hash) it
|
|
80
|
-
case memory
|
|
81
|
-
when Memory then memory
|
|
82
|
-
when Hash then run_memory.tap { |m| m.merge!(memory) }
|
|
83
|
-
else run_memory
|
|
84
|
-
end
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
`run` then sets `run_memory.current_writer = @name` for the duration of the run and restores the previous writer in an `ensure` block, so subscription callbacks always see the robot that actually wrote a value.
|
|
88
|
-
|
|
89
|
-
The whole run body is wrapped in `RobotLab::Hooks.run(:run, context, ...)` against the registries `[RobotLab.hooks, network&.hooks, @hooks]`.
|
|
90
|
-
|
|
91
|
-
### 2. MCP Hierarchy Resolution
|
|
92
|
-
|
|
93
|
-
MCP servers and tools are resolved together in `prepare_tools`, through a hierarchy: **runtime (run/task) > robot build-time > network > global config**.
|
|
94
|
-
|
|
95
|
-
```ruby
|
|
96
|
-
# Parent value: task/network RunConfig, then the network object, then global config
|
|
97
|
-
parent_value = network_config&.mcp || network_parent_config(network)&.mcp || RobotLab.config.mcp
|
|
98
|
-
build_resolved = ToolConfig.resolve_mcp(@mcp_config, parent_value: parent_value)
|
|
99
|
-
|
|
100
|
-
# Then resolve the runtime override against the build-time value
|
|
101
|
-
resolved_mcp = ToolConfig.resolve_mcp(runtime_mcp, parent_value: build_resolved)
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
Values at each level:
|
|
105
|
-
|
|
106
|
-
- `:none` -- no MCP servers at this level
|
|
107
|
-
- `:inherit` -- use parent level's MCP config
|
|
108
|
-
- `Array` -- explicit list of server configurations
|
|
109
|
-
|
|
110
|
-
`run`'s default for `mcp:` is `:none`, so a plain `robot.run("...")` resolves to an empty server list and connects nothing, even when `mcp:` was supplied at build time. Pass `mcp: :inherit` to trigger the connection. (`robot.connect_mcp!` connects eagerly against the build-time config, which is useful for reporting connection status at startup.)
|
|
111
|
-
|
|
112
|
-
### 3. MCP Client Initialization
|
|
113
|
-
|
|
114
|
-
If MCP servers need to be connected (or reconnected), the robot initializes clients:
|
|
115
|
-
|
|
116
|
-
```ruby
|
|
117
|
-
# Connect to each MCP server (ensure_mcp_clients -> init_mcp_client)
|
|
118
|
-
client = MCP::Client.new(server_config) # config is POSITIONAL
|
|
119
|
-
client.connect
|
|
120
|
-
|
|
121
|
-
if client.connected?
|
|
122
|
-
@mcp_clients[client.server.name] = client
|
|
123
|
-
discover_mcp_tools(client, server_name) # each remote tool becomes a Tool.create wrapper
|
|
124
|
-
else
|
|
125
|
-
@failed_mcp_configs[server_name] = server_config
|
|
126
|
-
end
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
Connection failures are logged and recorded, never raised — they surface through `robot.failed_mcp_server_names`. A later run retries only the servers that are still needed and still failed.
|
|
130
|
-
|
|
131
|
-
### 4. Tools Resolution
|
|
132
|
-
|
|
133
|
-
Tools are resolved through the same hierarchy, filtered, capped, and applied to the chat:
|
|
134
|
-
|
|
135
|
-
```ruby
|
|
136
|
-
# Explicit :none (or a literal []) means "send zero tools this turn"; otherwise
|
|
137
|
-
# filter the local + MCP tools by the resolved allowlist and clamp to max_tools.
|
|
138
|
-
filtered = explicit_none_tools?(tools) ? [] : cap_tools(filtered_tools(resolved_tools))
|
|
139
|
-
|
|
140
|
-
@chat.with_tools(*filtered, replace: true) if filtered.any? || explicit_none_tools?(tools)
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
Two details here are load-bearing:
|
|
144
|
-
|
|
145
|
-
- **`replace: true`** — RubyLLM's `with_tools` appends by default. On a persistent chat that would let tools accumulate across turns, so the chat is made to hold exactly this turn's resolved set. An explicit `:none` therefore clears the chat's tools to zero.
|
|
146
|
-
- **`cap_tools`** — the resolved list is clamped to `RunConfig#max_tools`, which defaults to `DEFAULT_MAX_TOOLS` (128) because most providers reject longer tool arrays. Setting `max_tools` to nil, 0, or a negative number falls back to 128; the cap cannot be disabled. Dropped tools are logged at `warn`.
|
|
147
|
-
|
|
148
|
-
Because `run` defaults to `tools: :none`, a plain `robot.run("...")` sends the LLM no tools at all. Pass `tools: :inherit` to send the robot's attached tools; pass an explicit array of names to use it as an allowlist.
|
|
149
|
-
|
|
150
|
-
### 5. LLM Inference
|
|
151
|
-
|
|
152
|
-
The message is sent to the LLM via `Agent#ask`, which delegates to `@chat.ask`. `invoke_ask` wraps that call in the `:llm_generation` hook and installs the per-run guards first:
|
|
153
|
-
|
|
154
|
-
```ruby
|
|
155
|
-
RobotLab::Hooks.run(:llm_generation, generation_context, registries: ..., per_run_hooks: hooks) do
|
|
156
|
-
effective_message = inject_learnings(generation_context.request)
|
|
157
|
-
maybe_compact(network: context.network)
|
|
158
|
-
install_circuit_breaker if @config.max_tool_rounds
|
|
159
|
-
install_doom_loop_detection
|
|
160
|
-
ask(effective_message, **kwargs.slice(:with), &effective_streaming_block(block))
|
|
161
|
-
end
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
Notes on the surrounding behavior:
|
|
165
|
-
|
|
166
|
-
- `:llm_generation` fires **exactly once per `robot.run`** — the provider's tool loop happens inside the block, not once per API call.
|
|
167
|
-
- Only `:with` from `run`'s keyword arguments reaches `ask`. Every other unrecognized keyword is treated as template re-render context.
|
|
168
|
-
- Doom-loop detection is installed unconditionally on every run; `doom_loop_threshold:` (default 3) only tunes it.
|
|
169
|
-
|
|
170
|
-
The persistent `@chat` (a `RubyLLM::Chat` instance) handles:
|
|
171
|
-
|
|
172
|
-
- Maintaining conversation history
|
|
173
|
-
- Sending the system prompt
|
|
174
|
-
- Formatting messages for the provider
|
|
175
|
-
- Executing the tool call loop automatically
|
|
176
|
-
|
|
177
|
-
### 6. Tool Execution Loop
|
|
178
|
-
|
|
179
|
-
RubyLLM's `@chat` handles the tool loop automatically. When the LLM requests a tool call:
|
|
180
|
-
|
|
181
|
-
1. `@chat` identifies the tool from its registered tools
|
|
182
|
-
2. Calls the tool's `execute` method (for `RubyLLM::Tool` subclasses) or `call` method (for `RobotLab::Tool`)
|
|
183
|
-
3. Sends the result back to the LLM
|
|
184
|
-
4. Repeats until the LLM produces a final text response
|
|
185
|
-
|
|
186
|
-
The `on_tool_call` and `on_tool_result` callbacks fire during this loop if configured. They are read off the effective `RunConfig` and registered on `@chat` during `Robot#initialize`:
|
|
187
|
-
|
|
188
|
-
```ruby
|
|
189
|
-
def register_chat_callbacks
|
|
190
|
-
@chat.on_tool_call(&@on_tool_call) if @on_tool_call
|
|
191
|
-
@chat.on_tool_result(&@on_tool_result) if @on_tool_result
|
|
192
|
-
setup_bus_channel if @bus
|
|
193
|
-
end
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
When `max_tool_rounds` is set, `install_circuit_breaker` temporarily wraps `on_tool_call` for the duration of the run and raises `ToolLoopError` once the round count is exceeded; the original callback is restored in `run`'s `ensure` block.
|
|
197
|
-
|
|
198
|
-
### 7. Result Construction
|
|
199
|
-
|
|
200
|
-
After the LLM responds, `build_result` turns the response into a `RobotResult`:
|
|
201
|
-
|
|
202
|
-
```ruby
|
|
203
|
-
def build_result(response, _memory)
|
|
204
|
-
text = result_text(response)
|
|
205
|
-
output = text ? [TextMessage.new(role: 'assistant', content: text)] : []
|
|
206
|
-
# ... reads response.tokens (or input_tokens/output_tokens), accumulates them
|
|
207
|
-
# into the robot's running totals, and passes them to RobotResult
|
|
208
|
-
end
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
`result_text` is deliberately more forgiving than `response.content`. It falls back, in order, to:
|
|
212
|
-
|
|
213
|
-
1. `response.content`, when present and non-empty
|
|
214
|
-
2. `response.thinking` — Ollama routes some models' (e.g. qwen3) entire output through `reasoning_content`, which RubyLLM surfaces as `thinking`
|
|
215
|
-
3. the most recent non-empty assistant message in `@chat.messages`, scoped to messages *after* the last user message, for models that end a turn on a tool call with no trailing text
|
|
216
|
-
|
|
217
|
-
Scoping the history fallback to the current turn is what prevents a previous turn's answer from being returned when a thinking-mode model emits nothing in `content`.
|
|
218
|
-
|
|
219
|
-
`build_result` also does token accounting: it reads `response.tokens` (falling back to `input_tokens`/`output_tokens`), adds them to the robot's running totals, and stores them on the result. `run` then reconciles the budget reservation and enforces `token_budget` / `cost_budget`, raising **`RobotLab::InferenceError`** when this call's actual usage pushed cumulative usage over budget. (`RobotLab::BudgetExceeded` is the *other* budget error: it is raised by `reserve_budget!` **before** the call, when a prior call already exhausted the dimension. See [Budgets](../guides/observability.md#budgets-token-cost).)
|
|
220
|
-
|
|
221
|
-
## RobotResult
|
|
222
|
-
|
|
223
|
-
The result object from a `robot.run` call:
|
|
224
|
-
|
|
225
|
-
```ruby
|
|
226
|
-
result = robot.run("Hello!")
|
|
227
|
-
|
|
228
|
-
result.robot_name # => "assistant"
|
|
229
|
-
result.output # => [TextMessage] synthesized from result_text
|
|
230
|
-
result.tool_calls # => [] in practice (see below)
|
|
231
|
-
result.stop_reason # => nil — always (see below)
|
|
232
|
-
result.created_at # => Time
|
|
233
|
-
result.id # => UUID string
|
|
234
|
-
result.duration # => Float or nil (elapsed seconds, set in pipeline execution)
|
|
235
|
-
result.input_tokens # => Integer
|
|
236
|
-
result.output_tokens # => Integer
|
|
237
|
-
result.checksum # => "sha256-hex"
|
|
238
|
-
result.raw # => raw LLM response object
|
|
239
|
-
|
|
240
|
-
# Convenience methods
|
|
241
|
-
result.last_text_content # => "Hi there!" (last text message content)
|
|
242
|
-
result.reply # => alias for last_text_content
|
|
243
|
-
result.has_tool_calls? # => false
|
|
244
|
-
result.stopped? # => true
|
|
245
|
-
result.export # => Hash (excludes debug fields)
|
|
246
|
-
result.to_h / result.to_json
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
`stop_reason` is always `nil`. `build_result` assigns it with `response.respond_to?(:stop_reason) ? response.stop_reason : nil`, and `RubyLLM::Message` does not define `stop_reason`, so no `robot.run` result ever reports one. It is dropped from `export` by the `.compact`, and `stopped?` therefore reduces to `!has_tool_calls?`.
|
|
250
|
-
|
|
251
|
-
`output` is a single synthesized `TextMessage`, not a transcript of the turn, and `tool_calls` is read off the final assistant message — which no longer carries tool calls once RubyLLM's tool loop has finished, so it is effectively always empty. Observe tool usage through the `on_tool_call`/`on_tool_result` callbacks or the tool hooks instead.
|
|
252
|
-
|
|
253
|
-
## Streaming
|
|
254
|
-
|
|
255
|
-
Robots support streaming by passing a block to `run`. The block receives a `RubyLLM::Chunk`; its text is in `content`:
|
|
256
|
-
|
|
257
|
-
```ruby
|
|
258
|
-
result = robot.run("Tell me a story") do |chunk|
|
|
259
|
-
print chunk.content
|
|
260
|
-
end
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
`effective_streaming_block` merges the block with any stored `on_content:` callback. If both are present, both fire — the stored `on_content` first, then the runtime block. The merged block is forwarded to `Agent#ask`, which passes it to `@chat.ask`.
|
|
264
|
-
|
|
265
|
-
```ruby
|
|
266
|
-
# on_content fires on every run, no block required
|
|
267
|
-
robot = RobotLab.build(name: "bot", system_prompt: "...",
|
|
268
|
-
on_content: ->(chunk) { print chunk.content })
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
`on_content` is read from the robot's own config at construction — a network-level `config:` does not supply it.
|
|
272
|
-
|
|
273
|
-
## Template Resolution
|
|
274
|
-
|
|
275
|
-
When a robot has a `template:`, it is resolved during initialization:
|
|
276
|
-
|
|
277
|
-
```ruby
|
|
278
|
-
# 1. Parse the template via prompt_manager
|
|
279
|
-
parsed = PM.parse(@template)
|
|
280
|
-
|
|
281
|
-
# 2. Apply the non-LLM front matter keys: robot_name, description, tools, mcp
|
|
282
|
-
# (constructor-provided values win, so these only fill in gaps)
|
|
283
|
-
apply_front_matter_extras(parsed.metadata)
|
|
284
|
-
|
|
285
|
-
# 3. Turn the LLM keys into a RunConfig. Front matter is the BASE;
|
|
286
|
-
# @config (constructor kwargs) merges over it and wins.
|
|
287
|
-
effective = RunConfig.from_front_matter(parsed.metadata).merge(@config)
|
|
288
|
-
effective.apply_to(@chat, provider: @provider, assume_model_exists: !@provider.nil?)
|
|
289
|
-
|
|
290
|
-
# 4. Render the template body with context and set it as system instructions
|
|
291
|
-
@chat.with_instructions(parsed.to_s(**resolved_context))
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
If required parameters are missing at build time, rendering is skipped and deferred: the first `run` that supplies context calls `rerender_template`, which re-renders and re-appends the inline `system_prompt`. When `skills:` are in play, each skill's body and front matter are accumulated the same way and the bodies are joined before being set as one instructions block.
|
|
295
|
-
|
|
296
|
-
### Front Matter Config Keys
|
|
297
|
-
|
|
298
|
-
Templates parse these LLM keys into a `RunConfig`:
|
|
299
|
-
|
|
300
|
-
| Key | Effect |
|
|
301
|
-
|-----|--------|
|
|
302
|
-
| `model` | Sets the LLM model |
|
|
303
|
-
| `temperature` | Sets randomness |
|
|
304
|
-
| `top_p` | Parsed, but **not applied** — see below |
|
|
305
|
-
| `top_k` | Parsed, but **not applied** |
|
|
306
|
-
| `max_tokens` | Parsed, but **not applied** |
|
|
307
|
-
| `presence_penalty` | Parsed, but **not applied** |
|
|
308
|
-
| `frequency_penalty` | Parsed, but **not applied** |
|
|
309
|
-
| `stop` | Parsed, but **not applied** |
|
|
310
|
-
|
|
311
|
-
`RunConfig#apply_to` dispatches `chat.with_<field>` guarded by `respond_to?`. `RubyLLM::Chat` only defines `with_model` and `with_temperature`, so the other six are silently dropped when they come from front matter. To set them, use constructor kwargs or a `config:` RunConfig — that path goes through `apply_chat_params` → `with_params` and does take effect:
|
|
312
|
-
|
|
313
|
-
```ruby
|
|
314
|
-
RobotLab.build(name: "bot", template: :report, max_tokens: 2000, top_p: 0.3)
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
Front matter keys that are not LLM fields — `description`, `robot_name`, `tools`, `mcp`, `skills`, `parameters` — all work.
|
|
318
|
-
|
|
319
|
-
Template bodies render with ERB: use `<%= var %>`. `{{ var }}` is not interpolated and passes through verbatim.
|
|
320
|
-
|
|
321
|
-
## Model Selection
|
|
322
|
-
|
|
323
|
-
The model is determined by:
|
|
324
|
-
|
|
325
|
-
1. Robot's explicit `model:` parameter
|
|
326
|
-
2. Front matter `model` from template
|
|
327
|
-
3. Global `RobotLab.config.ruby_llm.model`
|
|
328
|
-
|
|
329
|
-
```ruby
|
|
330
|
-
robot = RobotLab.build(
|
|
331
|
-
name: "bot",
|
|
332
|
-
model: "claude-sonnet-4" # Takes precedence
|
|
333
|
-
)
|
|
334
|
-
|
|
335
|
-
# Or configure globally via config files / environment variables
|
|
336
|
-
# ROBOT_LAB_RUBY_LLM__MODEL=gpt-4o
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
## SimpleFlow Integration
|
|
340
|
-
|
|
341
|
-
When a robot runs inside a network, the `call` method is invoked by SimpleFlow:
|
|
342
|
-
|
|
343
|
-
```mermaid
|
|
344
|
-
sequenceDiagram
|
|
345
|
-
participant SF as SimpleFlow
|
|
346
|
-
participant Task as Task Wrapper
|
|
347
|
-
participant Robot
|
|
348
|
-
participant Chat as @chat
|
|
349
|
-
|
|
350
|
-
SF->>Task: call(result)
|
|
351
|
-
Task->>Task: Hooks.run(:task, ...)
|
|
352
|
-
Task->>Task: deep_merge(run_params, task_context)
|
|
353
|
-
Task->>Robot: call(enhanced_result)
|
|
354
|
-
Robot->>Robot: extract_run_context(result)
|
|
355
|
-
Robot->>Robot: message = context.delete(:message)
|
|
356
|
-
Robot->>Robot: run(message, **context)
|
|
357
|
-
Robot->>Chat: ask(message)
|
|
358
|
-
Chat-->>Robot: response
|
|
359
|
-
Robot-->>SF: result.continue(robot_result)
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
The `Task` wrapper runs the `:task` hook family and deep-merges its per-task configuration (context, mcp, tools, memory, config) into `run_params` before delegating to the robot's `call`. Note that a task's `mcp:`/`tools:` land in `run_params` and are therefore consumed by the robot as *runtime* values, not as a separate tier between the network and the robot.
|
|
363
|
-
|
|
364
|
-
The base `Robot#call` extracts the message, calls `run`, and records the elapsed time in `RobotResult#duration`. Because the result is stored under `@name.to_sym`, the pipeline context is keyed by the **robot's** name, not the task name. If the robot raises any exception, the error is caught and wrapped in a `RobotResult` so one failing robot does not crash the entire pipeline:
|
|
365
|
-
|
|
366
|
-
```ruby
|
|
367
|
-
def call(result)
|
|
368
|
-
run_context = extract_run_context(result)
|
|
369
|
-
message = run_context.delete(:message)
|
|
370
|
-
|
|
371
|
-
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
372
|
-
robot_result = run(message, **run_context)
|
|
373
|
-
robot_result.duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
374
|
-
|
|
375
|
-
result
|
|
376
|
-
.with_context(@name.to_sym, robot_result)
|
|
377
|
-
.continue(robot_result)
|
|
378
|
-
rescue Exception => e
|
|
379
|
-
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
380
|
-
error_result = RobotResult.new(
|
|
381
|
-
robot_name: @name,
|
|
382
|
-
output: [TextMessage.new(role: 'assistant', content: "Error: #{e.class}: #{e.message}")]
|
|
383
|
-
)
|
|
384
|
-
error_result.duration = elapsed
|
|
385
|
-
|
|
386
|
-
result
|
|
387
|
-
.with_context(@name.to_sym, error_result)
|
|
388
|
-
.continue(error_result)
|
|
389
|
-
end
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
## Next Steps
|
|
393
|
-
|
|
394
|
-
- [Network Orchestration](network-orchestration.md) - Multi-robot coordination
|
|
395
|
-
- [Core Concepts](core-concepts.md) - Fundamental building blocks
|
|
396
|
-
- [Using Tools](../guides/using-tools.md) - Creating and using tools
|