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,575 +0,0 @@
|
|
|
1
|
-
# Core Concepts
|
|
2
|
-
|
|
3
|
-
This page provides an in-depth look at RobotLab's fundamental building blocks.
|
|
4
|
-
|
|
5
|
-
## Robot
|
|
6
|
-
|
|
7
|
-
A Robot is the primary unit of computation in RobotLab. It is a subclass of `RubyLLM::Agent` that wraps a persistent `@chat` with:
|
|
8
|
-
|
|
9
|
-
- A unique identity (name, description)
|
|
10
|
-
- A personality (system prompt and/or template)
|
|
11
|
-
- Capabilities (tools, MCP connections)
|
|
12
|
-
- Model, provider, and inference configuration
|
|
13
|
-
- Inherent memory (key-value store)
|
|
14
|
-
|
|
15
|
-
### Robot Anatomy
|
|
16
|
-
|
|
17
|
-
```ruby
|
|
18
|
-
robot = RobotLab.build(
|
|
19
|
-
name: "support_agent", # Unique identifier
|
|
20
|
-
description: "Handles support requests", # Used for routing hints
|
|
21
|
-
model: "claude-sonnet-4", # LLM model
|
|
22
|
-
system_prompt: <<~PROMPT, # Inline system prompt
|
|
23
|
-
You are a friendly customer support agent for Acme Corp.
|
|
24
|
-
Always be polite and helpful. If you don't know something,
|
|
25
|
-
say so honestly.
|
|
26
|
-
PROMPT
|
|
27
|
-
local_tools: [OrderLookup, RefundProcessor], # RubyLLM::Tool subclasses
|
|
28
|
-
mcp: :inherit, # Use the network's MCP servers
|
|
29
|
-
temperature: 0.7 # Inference parameter
|
|
30
|
-
)
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
The build-time `mcp: :inherit` above assumes this robot will be added to a
|
|
34
|
-
network whose `config:` supplies an `mcp:` list. The parent is resolved on every
|
|
35
|
-
run, so `:inherit` picks up whatever the enclosing network provides. For a robot
|
|
36
|
-
that will run **standalone**, `:inherit` resolves against the global default
|
|
37
|
-
`:none` and yields nothing — give it an explicit array instead.
|
|
38
|
-
|
|
39
|
-
Or with a template:
|
|
40
|
-
|
|
41
|
-
```ruby
|
|
42
|
-
robot = RobotLab.build(
|
|
43
|
-
name: "support_agent",
|
|
44
|
-
template: :support, # Loads prompts/support.md
|
|
45
|
-
context: { company: "Acme Corp" }, # Template variables
|
|
46
|
-
local_tools: [OrderLookup]
|
|
47
|
-
)
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
### Robot Lifecycle
|
|
51
|
-
|
|
52
|
-
```mermaid
|
|
53
|
-
stateDiagram-v2
|
|
54
|
-
[*] --> Created: RobotLab.build / Robot.new
|
|
55
|
-
Created --> Running: robot.run("message")
|
|
56
|
-
Running --> ToolLoop: tool_call from LLM
|
|
57
|
-
ToolLoop --> Running: tool result sent back
|
|
58
|
-
Running --> Completed: final text response
|
|
59
|
-
Completed --> Running: robot.run("next message")
|
|
60
|
-
Completed --> [*]: robot.disconnect
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
The persistent `@chat` maintains conversation history across multiple `run` calls, making the robot stateful.
|
|
64
|
-
|
|
65
|
-
### Robot Properties
|
|
66
|
-
|
|
67
|
-
| Property | Type | Description |
|
|
68
|
-
|----------|------|-------------|
|
|
69
|
-
| `name` | `String` | Unique identifier within network |
|
|
70
|
-
| `description` | `String`, `nil` | What the robot does |
|
|
71
|
-
| `model` | `String` | LLM model ID (resolved from chat) |
|
|
72
|
-
| `template` | `Symbol`, `nil` | Prompt template identifier |
|
|
73
|
-
| `system_prompt` | `String`, `nil` | Inline system prompt |
|
|
74
|
-
| `local_tools` | `Array` | Locally defined tools |
|
|
75
|
-
| `mcp_clients` | `Hash` | Connected MCP clients by server name |
|
|
76
|
-
| `mcp_tools` | `Array` | Tools discovered from MCP servers |
|
|
77
|
-
| `memory` | `Memory` | Inherent key-value memory |
|
|
78
|
-
| `bus` | `TypedBus::MessageBus`, `nil` | Message bus instance |
|
|
79
|
-
| `outbox` | `Hash` | Sent messages tracked with status and replies |
|
|
80
|
-
| `mcp_config` | `Symbol`, `Array` | Build-time MCP configuration |
|
|
81
|
-
| `tools_config` | `Symbol`, `Array` | Build-time tools configuration |
|
|
82
|
-
|
|
83
|
-
### Running a Robot
|
|
84
|
-
|
|
85
|
-
The primary method is `robot.run("message")`:
|
|
86
|
-
|
|
87
|
-
```ruby
|
|
88
|
-
result = robot.run("What is the weather in Berlin?")
|
|
89
|
-
puts result.last_text_content
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
`run` defaults to `mcp: :none, tools: :none`. Those defaults are *explicit* "send nothing this turn" values, not "unset" — so a plain `run` connects no MCP servers and sends the LLM zero tools even when `local_tools:`/`mcp:` were supplied at build time. Opt in per run:
|
|
93
|
-
|
|
94
|
-
```ruby
|
|
95
|
-
robot.run("...", tools: :inherit) # send the attached local tools
|
|
96
|
-
robot.run("...", mcp: :inherit, tools: :inherit) # connect MCP servers and send their tools
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
With other runtime overrides:
|
|
100
|
-
|
|
101
|
-
```ruby
|
|
102
|
-
result = robot.run("Analyze this",
|
|
103
|
-
memory: { data: report },
|
|
104
|
-
tools: :inherit
|
|
105
|
-
)
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
With streaming — the block receives a `RubyLLM::Chunk`, whose text is in `content`:
|
|
109
|
-
|
|
110
|
-
```ruby
|
|
111
|
-
robot.run("Tell me a story") do |chunk|
|
|
112
|
-
print chunk.content
|
|
113
|
-
end
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
An `on_content:` callback (constructor kwarg or `RunConfig` field) fires on every run. When both an `on_content` callback and a block are supplied, both fire, stored callback first.
|
|
117
|
-
|
|
118
|
-
## Tool
|
|
119
|
-
|
|
120
|
-
Tools give robots the ability to interact with external systems. There are two patterns for defining tools.
|
|
121
|
-
|
|
122
|
-
### RubyLLM::Tool Subclass (Primary)
|
|
123
|
-
|
|
124
|
-
```ruby
|
|
125
|
-
class GetWeather < RubyLLM::Tool
|
|
126
|
-
description "Get current weather for a location"
|
|
127
|
-
|
|
128
|
-
param :location, type: "string", desc: "City name"
|
|
129
|
-
param :unit, type: "string", desc: "celsius or fahrenheit"
|
|
130
|
-
|
|
131
|
-
def execute(location:, unit: "celsius")
|
|
132
|
-
WeatherAPI.current(location, unit: unit)
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
Tools defined as `RubyLLM::Tool` subclasses are passed to robots via `local_tools:`:
|
|
138
|
-
|
|
139
|
-
```ruby
|
|
140
|
-
robot = RobotLab.build(
|
|
141
|
-
name: "weather_bot",
|
|
142
|
-
system_prompt: "You provide weather information.",
|
|
143
|
-
local_tools: [GetWeather]
|
|
144
|
-
)
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
### RobotLab::Tool.create Factory
|
|
148
|
-
|
|
149
|
-
For simpler tools that do not need a class:
|
|
150
|
-
|
|
151
|
-
```ruby
|
|
152
|
-
tool = RobotLab::Tool.create(
|
|
153
|
-
name: "get_time",
|
|
154
|
-
description: "Get the current time"
|
|
155
|
-
) { |_args| Time.now.to_s }
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
With parameter schema:
|
|
159
|
-
|
|
160
|
-
```ruby
|
|
161
|
-
tool = RobotLab::Tool.create(
|
|
162
|
-
name: "get_weather",
|
|
163
|
-
description: "Get weather for a location",
|
|
164
|
-
parameters: {
|
|
165
|
-
type: "object",
|
|
166
|
-
properties: {
|
|
167
|
-
location: { type: "string", description: "City name" }
|
|
168
|
-
},
|
|
169
|
-
required: ["location"]
|
|
170
|
-
}
|
|
171
|
-
) { |args| WeatherAPI.current(args[:location]) }
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
### Tool Execution
|
|
175
|
-
|
|
176
|
-
When an LLM decides to use a tool:
|
|
177
|
-
|
|
178
|
-
1. LLM generates a tool call with tool name and arguments
|
|
179
|
-
2. `@chat` (RubyLLM) identifies the tool from its registered tools
|
|
180
|
-
3. Calls the `execute` method with keyword arguments
|
|
181
|
-
4. Result is sent back to the LLM for continued processing
|
|
182
|
-
5. Loop repeats until the LLM produces a final text response
|
|
183
|
-
|
|
184
|
-
### Error Handling
|
|
185
|
-
|
|
186
|
-
Tool errors are captured and returned to the LLM:
|
|
187
|
-
|
|
188
|
-
```ruby
|
|
189
|
-
def execute(order_id:)
|
|
190
|
-
order = ORDERS[order_id]
|
|
191
|
-
if order
|
|
192
|
-
order
|
|
193
|
-
else
|
|
194
|
-
{ error: "Order not found" }
|
|
195
|
-
end
|
|
196
|
-
end
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
## Memory
|
|
200
|
-
|
|
201
|
-
Memory is a reactive key-value store used by robots and networks.
|
|
202
|
-
|
|
203
|
-
### Standalone vs Network Memory
|
|
204
|
-
|
|
205
|
-
- **Standalone**: Each robot has its own inherent `Memory` instance (`robot.memory`)
|
|
206
|
-
- **In a Network**: All robots share the network's `Memory` instance
|
|
207
|
-
|
|
208
|
-
```ruby
|
|
209
|
-
# Standalone memory
|
|
210
|
-
robot.memory[:user_id] = 123
|
|
211
|
-
robot.memory[:user_id] # => 123
|
|
212
|
-
|
|
213
|
-
# Network memory is passed automatically
|
|
214
|
-
network = RobotLab.create_network(name: "pipeline") do
|
|
215
|
-
task :robot_a, robot_a, depends_on: :none
|
|
216
|
-
task :robot_b, robot_b, depends_on: [:robot_a]
|
|
217
|
-
end
|
|
218
|
-
# Both robot_a and robot_b share network.memory during execution
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
### Reserved Keys
|
|
222
|
-
|
|
223
|
-
| Key | Type | Description |
|
|
224
|
-
|-----|------|-------------|
|
|
225
|
-
| `:data` | `StateProxy` | Runtime data (hash-style `memory.data[:key]` and method-style `memory.data.key_name`) |
|
|
226
|
-
| `:results` | `Array` | Accumulated robot results |
|
|
227
|
-
| `:messages` | `Array` | Conversation history |
|
|
228
|
-
| `:session_id` | `String` | Session identifier |
|
|
229
|
-
| `:cache` | `Module`, `nil` | Semantic cache (RubyLLM::SemanticCache when the optional gem is installed) |
|
|
230
|
-
|
|
231
|
-
### Reactive Features
|
|
232
|
-
|
|
233
|
-
Memory supports pub/sub semantics for inter-robot communication:
|
|
234
|
-
|
|
235
|
-
```ruby
|
|
236
|
-
# Write a value (notifies subscribers, wakes waiters)
|
|
237
|
-
memory.set(:sentiment, { score: 0.8 })
|
|
238
|
-
|
|
239
|
-
# Read a value (non-blocking)
|
|
240
|
-
memory.get(:sentiment) # => { score: 0.8 } or nil
|
|
241
|
-
|
|
242
|
-
# Blocking read (waits until value exists)
|
|
243
|
-
memory.get(:sentiment, wait: true) # Blocks indefinitely
|
|
244
|
-
memory.get(:sentiment, wait: 30) # Blocks up to 30s, then raises RobotLab::AwaitTimeout
|
|
245
|
-
|
|
246
|
-
# Subscribe to changes
|
|
247
|
-
memory.subscribe(:sentiment) do |change|
|
|
248
|
-
puts "#{change.key} = #{change.value} (written by #{change.writer})"
|
|
249
|
-
end
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
## Message Types
|
|
253
|
-
|
|
254
|
-
RobotLab uses a type hierarchy for messages:
|
|
255
|
-
|
|
256
|
-
```mermaid
|
|
257
|
-
classDiagram
|
|
258
|
-
Message <|-- TextMessage
|
|
259
|
-
Message <|-- ToolCallMessage
|
|
260
|
-
Message <|-- ToolResultMessage
|
|
261
|
-
|
|
262
|
-
class Message {
|
|
263
|
-
+String type
|
|
264
|
-
+String role
|
|
265
|
-
+content
|
|
266
|
-
+String stop_reason
|
|
267
|
-
+text?()
|
|
268
|
-
+tool_call?()
|
|
269
|
-
+tool_result?()
|
|
270
|
-
+stopped?()
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
class TextMessage {
|
|
274
|
-
+String content
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
class ToolCallMessage {
|
|
278
|
-
+Array~ToolMessage~ tools
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
class ToolResultMessage {
|
|
282
|
-
+ToolMessage tool
|
|
283
|
-
+Hash content
|
|
284
|
-
+success?()
|
|
285
|
-
+error?()
|
|
286
|
-
}
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
### Message Roles
|
|
290
|
-
|
|
291
|
-
| Role | Description |
|
|
292
|
-
|------|-------------|
|
|
293
|
-
| `user` | Input from the user |
|
|
294
|
-
| `assistant` | Response from the LLM |
|
|
295
|
-
| `system` | System instructions |
|
|
296
|
-
| `tool_result` | Tool execution result |
|
|
297
|
-
|
|
298
|
-
### Stop Reasons
|
|
299
|
-
|
|
300
|
-
| Reason | Description |
|
|
301
|
-
|--------|-------------|
|
|
302
|
-
| `stop` | Natural completion |
|
|
303
|
-
| `tool` | Tool call requested |
|
|
304
|
-
|
|
305
|
-
## RobotResult
|
|
306
|
-
|
|
307
|
-
The output from a robot execution:
|
|
308
|
-
|
|
309
|
-
```ruby
|
|
310
|
-
result = robot.run("Hello!")
|
|
311
|
-
|
|
312
|
-
result.robot_name # => "support_agent"
|
|
313
|
-
result.output # => [TextMessage] built from the final response text
|
|
314
|
-
result.tool_calls # => [] — see note below
|
|
315
|
-
result.stop_reason # => nil — always (see below)
|
|
316
|
-
result.created_at # => Time
|
|
317
|
-
result.id # => UUID string
|
|
318
|
-
result.input_tokens # => Integer
|
|
319
|
-
result.output_tokens # => Integer
|
|
320
|
-
result.duration # => Float, nil (set by Robot#call during pipeline execution)
|
|
321
|
-
result.checksum # => "sha256-hex"
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
`stop_reason` is always `nil` on a `Robot#run` result — `build_result` only copies it when the response responds to `stop_reason`, and `RubyLLM::Message` does not. The table above describes `Message::VALID_STOP_REASONS`, which applies to message objects you construct yourself, not to `RobotResult`. As a consequence `result.stopped?` reduces to `!result.has_tool_calls?`.
|
|
325
|
-
|
|
326
|
-
`output` is always a single `TextMessage` synthesized from the final response text — it is not a transcript of the turn. `tool_calls` is read off that final assistant message, which no longer carries tool calls once RubyLLM's tool loop has finished, so in practice it is empty; use the `on_tool_call`/`on_tool_result` callbacks or the tool hooks to observe tool usage.
|
|
327
|
-
|
|
328
|
-
### Accessing Response Content
|
|
329
|
-
|
|
330
|
-
```ruby
|
|
331
|
-
# Get last text response (most common)
|
|
332
|
-
text = result.last_text_content
|
|
333
|
-
|
|
334
|
-
# Check if tools were called
|
|
335
|
-
has_tools = result.has_tool_calls?
|
|
336
|
-
|
|
337
|
-
# Check if execution completed naturally
|
|
338
|
-
result.stopped?
|
|
339
|
-
|
|
340
|
-
# Serialization
|
|
341
|
-
result.export # => Hash (excludes debug fields)
|
|
342
|
-
result.to_h # => Hash (includes debug fields)
|
|
343
|
-
result.to_json # => JSON string
|
|
344
|
-
```
|
|
345
|
-
|
|
346
|
-
## Configuration
|
|
347
|
-
|
|
348
|
-
Global configuration uses `MywayConfig`. Sources are layered lowest to highest:
|
|
349
|
-
|
|
350
|
-
1. Bundled defaults (`lib/robot_lab/config/defaults.yml`)
|
|
351
|
-
2. Environment-specific overrides (development / test / production)
|
|
352
|
-
3. XDG config file (`~/.config/robot_lab/robot_lab.yml` — note the filename repeats the app name; `config.yml` is never read, and this loader does **not** run ERB)
|
|
353
|
-
4. Project config (`./config/robot_lab.yml` — ERB is evaluated here)
|
|
354
|
-
5. Environment variables (`ROBOT_LAB_*` prefix, double underscore for nesting)
|
|
355
|
-
6. Constructor params
|
|
356
|
-
|
|
357
|
-
The two file layers treat top-level wrappers differently:
|
|
358
|
-
|
|
359
|
-
- **XDG file** (`~/.config/robot_lab/robot_lab.yml`) — a section named for the current environment **is** honoured. The loader checks `parsed.key?(env)` first (env comes from `Anyway::Settings.current_environment`, then `RAILS_ENV`, then `RACK_ENV`, defaulting to `"development"`) and falls back to the root when no such key exists. So `development:` works, a flat file works, and `production:` is simply skipped while you are in development. Only a `defaults:` wrapper is meaningless — it is not an environment name, so the whole hash is read as flat config and `defaults` is an unknown key.
|
|
360
|
-
- **Project file** (`./config/robot_lab.yml`) — outside Rails this must be flat; every wrapper, `defaults:` and environment names alike, is ignored. Inside Rails, `anyway_config` sets `current_environment` to `Rails.env`, which makes this file environmental: a flat project file is then ignored and keys must be nested under `development:` / `test:` / `production:`.
|
|
361
|
-
|
|
362
|
-
Read via `RobotLab.config`, or set values with the `RobotLab.configure` block, which yields the same `Config` object:
|
|
363
|
-
|
|
364
|
-
```ruby
|
|
365
|
-
RobotLab.config.ruby_llm.model # => "claude-sonnet-4"
|
|
366
|
-
RobotLab.config.ruby_llm.request_timeout # => 120
|
|
367
|
-
|
|
368
|
-
RobotLab.configure do |c|
|
|
369
|
-
c.logger = Logger.new($stdout)
|
|
370
|
-
end
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
### RunConfig
|
|
374
|
-
|
|
375
|
-
Global `Config` is distinct from `RunConfig`, the per-run settings object that flows `RobotLab.config → Network → Task → Robot → template front matter → constructor kwargs`. Its fields are:
|
|
376
|
-
|
|
377
|
-
- **LLM**: `model`, `temperature`, `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop`
|
|
378
|
-
- **Capabilities** (`TOOL_FIELDS`): `mcp`, `tools`
|
|
379
|
-
- **Callbacks**: `on_tool_call`, `on_tool_result`, `on_content`
|
|
380
|
-
- **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`
|
|
381
|
-
|
|
382
|
-
`TOOL_FIELDS` is exactly `[:mcp, :tools]` — those are the two a network propagates. `max_tools` is an infrastructure field, so it is *not* inherited that way.
|
|
383
|
-
|
|
384
|
-
Per robot, template front matter is the **base**, a `config:` RunConfig merges over it, and constructor kwargs always win. A network-level `config:` propagates only `mcp` and `tools` down to member robots (and only when a robot opts in with `:inherit`); LLM fields and callbacks are never inherited from a network. `max_concurrent_robots` is the one field the network itself consumes.
|
|
385
|
-
|
|
386
|
-
## Configuration Hierarchy
|
|
387
|
-
|
|
388
|
-
Tools and MCP servers use a cascading configuration system:
|
|
389
|
-
|
|
390
|
-
```
|
|
391
|
-
RobotLab.config (global)
|
|
392
|
-
|
|
|
393
|
-
+-- mcp: [server1, server2]
|
|
394
|
-
+-- tools: [tool1, tool2]
|
|
395
|
-
|
|
|
396
|
-
+-- Network config: (RunConfig)
|
|
397
|
-
| |
|
|
398
|
-
| +-- mcp: :inherit | :none | [servers]
|
|
399
|
-
| +-- tools: :inherit | :none | [tools]
|
|
400
|
-
| |
|
|
401
|
-
| +-- Robot (build-time config)
|
|
402
|
-
| |
|
|
403
|
-
| +-- mcp: :inherit | :none | [servers]
|
|
404
|
-
| +-- tools: :inherit | :none | [tools]
|
|
405
|
-
| |
|
|
406
|
-
| +-- Task (per-task) / run() call -- the RUNTIME level
|
|
407
|
-
| +-- mcp: :none (default) | :inherit | [servers]
|
|
408
|
-
| +-- tools: :none (default) | :inherit | [tools]
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
Resolution order: **task/runtime > robot build-time > network > global config**.
|
|
412
|
-
|
|
413
|
-
A `Task`'s `mcp:`/`tools:` are not a separate tier between network and robot. The task injects them into `run_params`, and `Robot#call` pulls them out and passes them to `run` — so they arrive as the *runtime* value and are resolved against the robot's build-time value.
|
|
414
|
-
|
|
415
|
-
The `:inherit` value pulls from the parent level. `:none` explicitly disables. An explicit array is a **filter over the already-attached tools**, not a local-vs-MCP switch.
|
|
416
|
-
|
|
417
|
-
Three consequences worth internalizing:
|
|
418
|
-
|
|
419
|
-
- `run()` defaults both to `:none`, so the runtime level is a deliberate "send nothing this turn" unless you override it. Pass `tools: :inherit` (and `mcp: :inherit`) to use what the robot was built with.
|
|
420
|
-
- Build-time `:inherit` depends on there being a parent to inherit from. The parent is recomputed on every run (`network_config&.tools || network_parent_config(network)&.tools || RobotLab.config.tools`), so inside a network whose `config:` sets `tools:`/`mcp:` it is exactly the right way to opt in. For a robot that runs **standalone** the parent is the global `:none`, so build-time `:inherit` matches nothing — leave `tools:` unset there and opt in at run time.
|
|
421
|
-
- An explicit array is compared with `tool.name.to_s`, and `Class#name` differs from `RubyLLM::Tool#name`. A tool attached as a class matches `[RefundTool]` (`"RefundTool"`); the same tool attached as an instance matches `%w[refund]`. Both forms work — but the array must be written in the same form the tool was attached in, or it filters everything out. Note that the **build-time** `tools:` kwarg is validated (`validate_tools_filter!`) and accepts only Strings/Symbols; the class form is available only at the task/runtime level, which is not validated. See [Network Orchestration](network-orchestration.md#task-configuration).
|
|
422
|
-
|
|
423
|
-
## Message Bus
|
|
424
|
-
|
|
425
|
-
The **Message Bus** provides bidirectional, cyclic communication between robots, independent of the Network pipeline. While Networks enforce DAG-based (acyclic) execution, the bus enables negotiation loops, convergence patterns, and multi-turn dialogues.
|
|
426
|
-
|
|
427
|
-
### How It Works
|
|
428
|
-
|
|
429
|
-
Robots connect to a shared `TypedBus::MessageBus` via the `bus:` parameter. Each robot gets a typed channel (accepting only `RobotMessage` objects) named after its `name`. Delivery is routed through a shared `BusPoller`, which runs the handler **in the caller's execution context** (Async fiber or OS thread) rather than on a background thread of its own. A mutex serializes deliveries per robot: if a robot is already processing a message, later ones are queued and drained after the current one returns.
|
|
430
|
-
|
|
431
|
-
```ruby
|
|
432
|
-
bus = TypedBus::MessageBus.new
|
|
433
|
-
|
|
434
|
-
bob = RobotLab.build(name: "bob", system_prompt: "You tell jokes.", bus: bus)
|
|
435
|
-
alice = RobotLab.build(name: "alice", system_prompt: "You evaluate jokes.", bus: bus)
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
### RobotMessage
|
|
439
|
-
|
|
440
|
-
`RobotMessage` is an immutable `Data.define` value object used as the typed envelope:
|
|
441
|
-
|
|
442
|
-
| Field | Type | Description |
|
|
443
|
-
|-------|------|-------------|
|
|
444
|
-
| `id` | `Integer` | Per-robot sequential counter |
|
|
445
|
-
| `from` | `String` | Sender's robot name (= channel name) |
|
|
446
|
-
| `content` | `String`, `Hash` | Message payload |
|
|
447
|
-
| `in_reply_to` | `String`, `nil` | Composite key of the original message (e.g., `"alice:1"`) |
|
|
448
|
-
|
|
449
|
-
Methods: `key` returns `"from:id"` composite identity; `reply?` returns true when `in_reply_to` is set.
|
|
450
|
-
|
|
451
|
-
### Sending and Receiving
|
|
452
|
-
|
|
453
|
-
```ruby
|
|
454
|
-
# Send a message to another robot
|
|
455
|
-
alice.send_message(to: :bob, content: "Tell me a joke.")
|
|
456
|
-
|
|
457
|
-
# Handle incoming messages with auto-ack (1 arg)
|
|
458
|
-
bob.on_message do |message|
|
|
459
|
-
joke = bob.run(message.content.to_s).last_text_content
|
|
460
|
-
bob.send_reply(to: message.from.to_sym, content: joke, in_reply_to: message.key)
|
|
461
|
-
end
|
|
462
|
-
```
|
|
463
|
-
|
|
464
|
-
Block arity controls delivery handling: 1 argument auto-acks; 2 arguments give manual control over `delivery.ack!`/`delivery.nack!`.
|
|
465
|
-
|
|
466
|
-
`send_message`/`send_reply` synchronize the per-robot message counter and outbox with an internal mutex, so concurrent sends from multiple threads and reply correlation can't clobber each other — the bus is safe to send on from more than one thread at a time. (There is no poller thread doing the correlating: as described above, `BusPoller#enqueue` processes deliveries inline in the caller's context.)
|
|
467
|
-
|
|
468
|
-
For the common case of a robot that should simply answer whatever tasks arrive on the bus, `respond_to_tasks`/`serve` do the `on_message` wiring above in one call:
|
|
469
|
-
|
|
470
|
-
```ruby
|
|
471
|
-
bob.serve # equivalent to the on_message block above, running bob.run and replying automatically
|
|
472
|
-
```
|
|
473
|
-
|
|
474
|
-
`respond_to_tasks` takes a block instead when the reply needs post-processing, and both ignore inbound messages that are themselves replies, so two robots calling `serve` on each other don't loop. See [Auto-Responding to Bus Tasks](../guides/building-robots.md#auto-responding-to-bus-tasks).
|
|
475
|
-
|
|
476
|
-
### Dynamic Spawning
|
|
477
|
-
|
|
478
|
-
Robots can create new robots at runtime using `spawn`. The bus is created lazily — no upfront wiring required:
|
|
479
|
-
|
|
480
|
-
```ruby
|
|
481
|
-
dispatcher = RobotLab.build(name: "dispatcher", system_prompt: "You delegate work.")
|
|
482
|
-
|
|
483
|
-
# spawn creates a child on the same bus (bus created automatically)
|
|
484
|
-
helper = dispatcher.spawn(name: "helper", system_prompt: "You answer questions.")
|
|
485
|
-
|
|
486
|
-
# The child can immediately communicate with the parent
|
|
487
|
-
answer = helper.run("What is 2+2?").last_text_content
|
|
488
|
-
helper.send_message(to: :dispatcher, content: answer)
|
|
489
|
-
```
|
|
490
|
-
|
|
491
|
-
The spawned robot inherits its parent's `model`/`provider`, so a dispatcher running on a local provider (e.g. Ollama) spawns specialists targeting that same model instead of falling back to the global default (which would fail without cloud credentials). Explicit `model:`/`provider:` passed to `spawn` still override.
|
|
492
|
-
|
|
493
|
-
Robots can also join a bus after creation using `with_bus`:
|
|
494
|
-
|
|
495
|
-
```ruby
|
|
496
|
-
bot = RobotLab.build(name: "late_joiner", system_prompt: "Hello.")
|
|
497
|
-
bot.with_bus(existing_bus) # now connected to the bus
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
**Fan-out messaging**: Multiple robots with the same name all subscribe to the same channel. Messages sent to that name are delivered to all subscribers:
|
|
501
|
-
|
|
502
|
-
```ruby
|
|
503
|
-
worker1 = dispatcher.spawn(name: "worker", system_prompt: "Worker 1")
|
|
504
|
-
worker2 = dispatcher.spawn(name: "worker", system_prompt: "Worker 2")
|
|
505
|
-
dispatcher.send_message(to: :worker, content: "Do this task")
|
|
506
|
-
# Both worker1 and worker2 receive the message
|
|
507
|
-
```
|
|
508
|
-
|
|
509
|
-
### Bus vs Network
|
|
510
|
-
|
|
511
|
-
| Feature | Network | Message Bus |
|
|
512
|
-
|---------|---------|-------------|
|
|
513
|
-
| Execution model | DAG (acyclic) | Cyclic, bidirectional |
|
|
514
|
-
| Communication | Sequential pipeline | Pub/sub channels |
|
|
515
|
-
| Memory | Shared network memory | Independent per-robot |
|
|
516
|
-
| Use case | Linear workflows | Negotiation, convergence |
|
|
517
|
-
|
|
518
|
-
The bus is purely additive — robots without `bus:` work exactly as before.
|
|
519
|
-
|
|
520
|
-
## Network
|
|
521
|
-
|
|
522
|
-
A Network orchestrates multiple robots in a pipeline workflow using SimpleFlow:
|
|
523
|
-
|
|
524
|
-
```ruby
|
|
525
|
-
network = RobotLab.create_network(name: "support") do
|
|
526
|
-
task :classifier, classifier_robot, depends_on: :none
|
|
527
|
-
task :billing, billing_robot, depends_on: :optional
|
|
528
|
-
task :technical, technical_robot, depends_on: :optional
|
|
529
|
-
end
|
|
530
|
-
|
|
531
|
-
result = network.run(message: "I need help with billing")
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
Networks provide:
|
|
535
|
-
|
|
536
|
-
- **DAG-based execution** via SimpleFlow with `depends_on:` for sequencing
|
|
537
|
-
- **Parallel execution** for tasks with the same dependencies
|
|
538
|
-
- **Optional tasks** activated dynamically by classifier robots
|
|
539
|
-
- **Shared memory** for inter-robot communication
|
|
540
|
-
- **Per-task configuration** via the `Task` wrapper
|
|
541
|
-
- **Broadcast messaging** for network-wide announcements
|
|
542
|
-
|
|
543
|
-
Robots can be added to a network without a pipeline task via `add_robot`, and removed again with `remove_robot(name)` (returns the removed robot, or `nil` if absent). `remove_robot` only drops the robot from the crew — it doesn't rewrite the pipeline, so avoid removing a robot that's still referenced by a task's `depends_on`.
|
|
544
|
-
|
|
545
|
-
## Runnable Protocol
|
|
546
|
-
|
|
547
|
-
`RobotLab::Runnable` is a shared interface implemented by both `Robot` and `Network`, so callers can treat either uniformly instead of branching on `is_a?(RobotLab::Network)`:
|
|
548
|
-
|
|
549
|
-
```ruby
|
|
550
|
-
def summarize(runnable)
|
|
551
|
-
runnable.crew.each { |r| puts r.name }
|
|
552
|
-
puts "chief: #{runnable.chief.name}"
|
|
553
|
-
puts runnable.network? ? "network of #{runnable.robot_count}" : "single robot"
|
|
554
|
-
end
|
|
555
|
-
|
|
556
|
-
summarize(robot) # crew: [robot], chief: robot, "single robot"
|
|
557
|
-
summarize(network) # crew: network.robots.values, chief: crew.first, "network of N"
|
|
558
|
-
```
|
|
559
|
-
|
|
560
|
-
| Method | Robot | Network |
|
|
561
|
-
|--------|-------|---------|
|
|
562
|
-
| `crew` | `[self]` | `robots.values` (pipeline order) |
|
|
563
|
-
| `chief` | `self` | `crew.first` |
|
|
564
|
-
| `robot_count` | `1` | `crew.size` |
|
|
565
|
-
| `network?` | `false` | `true` |
|
|
566
|
-
| `single?` | `true` | `false` |
|
|
567
|
-
| `run(message = nil, **opts)` | accepts a positional message (already did) | now also accepts a positional message, folded into `message:` — the keyword form still works |
|
|
568
|
-
|
|
569
|
-
`crew` is the only method implementers must define themselves; `chief`, `robot_count`, and `single?` all derive from it, and `network?` defaults to `false` unless overridden (as `Network` does).
|
|
570
|
-
|
|
571
|
-
## Next Steps
|
|
572
|
-
|
|
573
|
-
- [Robot Execution](robot-execution.md) - Detailed execution flow
|
|
574
|
-
- [Network Orchestration](network-orchestration.md) - Multi-robot coordination
|
|
575
|
-
- [Using Tools](../guides/using-tools.md) - Creating and using tools
|