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,476 +0,0 @@
|
|
|
1
|
-
# Multi-Robot Network
|
|
2
|
-
|
|
3
|
-
Customer service system with intelligent routing using SimpleFlow pipelines.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
This example demonstrates a multi-robot network where a classifier routes customer inquiries to specialized support robots using SimpleFlow's optional task activation.
|
|
8
|
-
|
|
9
|
-
The runnable version is
|
|
10
|
-
[`examples/03_network.rb`](https://github.com/MadBomber/robot_lab/blob/main/examples/03_network.rb);
|
|
11
|
-
the snippets here expand it with a fourth specialist and per-task configuration.
|
|
12
|
-
|
|
13
|
-
> [!IMPORTANT]
|
|
14
|
-
> Two names are in play and they are easy to confuse.
|
|
15
|
-
> `result.activate(:x)` takes a **task** name (the first argument to `task`), while
|
|
16
|
-
> `result.with_context(@name.to_sym, ...)` keys the context by the **robot's** name.
|
|
17
|
-
> Activating a task name that was never declared **raises `ArgumentError` and aborts
|
|
18
|
-
> the run** — it is not a silent no-op. Give each robot the same name as its task and
|
|
19
|
-
> both problems disappear.
|
|
20
|
-
|
|
21
|
-
## Complete Example
|
|
22
|
-
|
|
23
|
-
```ruby
|
|
24
|
-
#!/usr/bin/env ruby
|
|
25
|
-
|
|
26
|
-
require "bundler/setup"
|
|
27
|
-
require "robot_lab"
|
|
28
|
-
|
|
29
|
-
# Custom classifier that routes to specialists
|
|
30
|
-
class ClassifierRobot < RobotLab::Robot
|
|
31
|
-
def call(result)
|
|
32
|
-
context = extract_run_context(result)
|
|
33
|
-
message = context.delete(:message)
|
|
34
|
-
robot_result = run(message, **context)
|
|
35
|
-
|
|
36
|
-
new_result = result
|
|
37
|
-
.with_context(@name.to_sym, robot_result)
|
|
38
|
-
.continue(robot_result)
|
|
39
|
-
|
|
40
|
-
# Route based on classification
|
|
41
|
-
category = robot_result.last_text_content.to_s.strip.downcase
|
|
42
|
-
|
|
43
|
-
case category
|
|
44
|
-
when /billing/ then new_result.activate(:billing_agent)
|
|
45
|
-
when /technical/ then new_result.activate(:tech_agent)
|
|
46
|
-
when /account/ then new_result.activate(:account_agent)
|
|
47
|
-
else new_result.activate(:general_agent)
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Classifier robot
|
|
53
|
-
classifier = ClassifierRobot.new(
|
|
54
|
-
name: "classifier",
|
|
55
|
-
description: "Classifies customer inquiries",
|
|
56
|
-
system_prompt: <<~PROMPT,
|
|
57
|
-
You are a customer inquiry classifier. Analyze the customer's message
|
|
58
|
-
and respond with exactly ONE of these categories:
|
|
59
|
-
|
|
60
|
-
- BILLING (payment issues, invoices, refunds, subscriptions)
|
|
61
|
-
- TECHNICAL (bugs, errors, how-to questions, feature requests)
|
|
62
|
-
- ACCOUNT (login issues, profile changes, security concerns)
|
|
63
|
-
- GENERAL (everything else)
|
|
64
|
-
|
|
65
|
-
Respond with ONLY the category name, nothing else.
|
|
66
|
-
PROMPT
|
|
67
|
-
model: "claude-sonnet-4"
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
# Billing specialist
|
|
71
|
-
billing_agent = RobotLab.build(
|
|
72
|
-
name: "billing_agent",
|
|
73
|
-
description: "Handles billing inquiries",
|
|
74
|
-
system_prompt: <<~PROMPT,
|
|
75
|
-
You are a billing support specialist. You help customers with:
|
|
76
|
-
- Payment issues and refunds
|
|
77
|
-
- Invoice questions
|
|
78
|
-
- Subscription management
|
|
79
|
-
- Pricing inquiries
|
|
80
|
-
|
|
81
|
-
Be helpful, empathetic, and provide clear next steps.
|
|
82
|
-
PROMPT
|
|
83
|
-
model: "claude-sonnet-4"
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
# Technical support
|
|
87
|
-
tech_agent = RobotLab.build(
|
|
88
|
-
name: "tech_agent",
|
|
89
|
-
description: "Handles technical issues",
|
|
90
|
-
system_prompt: <<~PROMPT,
|
|
91
|
-
You are a technical support specialist. You help customers with:
|
|
92
|
-
- Bug reports and troubleshooting
|
|
93
|
-
- Feature explanations
|
|
94
|
-
- Integration questions
|
|
95
|
-
- Best practices
|
|
96
|
-
|
|
97
|
-
Ask clarifying questions when needed. Provide step-by-step solutions.
|
|
98
|
-
PROMPT
|
|
99
|
-
model: "claude-sonnet-4"
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
# Account specialist
|
|
103
|
-
account_agent = RobotLab.build(
|
|
104
|
-
name: "account_agent",
|
|
105
|
-
description: "Handles account issues",
|
|
106
|
-
system_prompt: <<~PROMPT,
|
|
107
|
-
You are an account support specialist. You help customers with:
|
|
108
|
-
- Login and authentication issues
|
|
109
|
-
- Profile and settings changes
|
|
110
|
-
- Security concerns
|
|
111
|
-
- Account recovery
|
|
112
|
-
|
|
113
|
-
Prioritize security while being helpful.
|
|
114
|
-
PROMPT
|
|
115
|
-
model: "claude-sonnet-4"
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
# General support
|
|
119
|
-
general_agent = RobotLab.build(
|
|
120
|
-
name: "general_agent",
|
|
121
|
-
description: "Handles general inquiries",
|
|
122
|
-
system_prompt: <<~PROMPT,
|
|
123
|
-
You are a general support agent. You help customers with:
|
|
124
|
-
- Product information
|
|
125
|
-
- General questions
|
|
126
|
-
- Feedback collection
|
|
127
|
-
- Routing to appropriate departments
|
|
128
|
-
|
|
129
|
-
Be friendly and informative.
|
|
130
|
-
PROMPT
|
|
131
|
-
model: "claude-sonnet-4"
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
# Create the network with optional task routing
|
|
135
|
-
network = RobotLab.create_network(name: "customer_service") do
|
|
136
|
-
task :classifier, classifier, depends_on: :none
|
|
137
|
-
task :billing_agent, billing_agent, depends_on: :optional
|
|
138
|
-
task :tech_agent, tech_agent, depends_on: :optional
|
|
139
|
-
task :account_agent, account_agent, depends_on: :optional
|
|
140
|
-
task :general_agent, general_agent, depends_on: :optional
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# Run the support system
|
|
144
|
-
puts "Customer Service System"
|
|
145
|
-
puts "=" * 50
|
|
146
|
-
puts
|
|
147
|
-
|
|
148
|
-
test_inquiries = [
|
|
149
|
-
"I was charged twice for my subscription last month",
|
|
150
|
-
"How do I reset my password?",
|
|
151
|
-
"The app crashes when I try to upload photos",
|
|
152
|
-
"What features are included in the pro plan?"
|
|
153
|
-
]
|
|
154
|
-
|
|
155
|
-
test_inquiries.each do |inquiry|
|
|
156
|
-
puts "Customer: #{inquiry}"
|
|
157
|
-
puts "-" * 50
|
|
158
|
-
|
|
159
|
-
result = network.run(message: inquiry)
|
|
160
|
-
|
|
161
|
-
# Show classification
|
|
162
|
-
if result.context[:classifier]
|
|
163
|
-
puts "Classification: #{result.context[:classifier].last_text_content}"
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
# Show specialist response
|
|
167
|
-
if result.value.is_a?(RobotLab::RobotResult)
|
|
168
|
-
puts "Handled by: #{result.value.robot_name}"
|
|
169
|
-
puts "Response: #{result.value.last_text_content[0..200]}..."
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
puts
|
|
173
|
-
puts "=" * 50
|
|
174
|
-
puts
|
|
175
|
-
end
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
## ClassifierRobot Pattern
|
|
179
|
-
|
|
180
|
-
The key to conditional routing is overriding the `call` method. The base `Robot#call` extracts the message from the `SimpleFlow::Result` and calls `run`. A classifier overrides this to inspect the output and activate the appropriate optional task:
|
|
181
|
-
|
|
182
|
-
```ruby
|
|
183
|
-
class ClassifierRobot < RobotLab::Robot
|
|
184
|
-
def call(result)
|
|
185
|
-
# 1. Extract run context from the SimpleFlow result
|
|
186
|
-
context = extract_run_context(result)
|
|
187
|
-
|
|
188
|
-
# 2. Pull out the message (it's a key in the context hash)
|
|
189
|
-
message = context.delete(:message)
|
|
190
|
-
|
|
191
|
-
# 3. Run the robot to get a classification
|
|
192
|
-
robot_result = run(message, **context)
|
|
193
|
-
|
|
194
|
-
# 4. Store our result and continue the pipeline
|
|
195
|
-
new_result = result
|
|
196
|
-
.with_context(@name.to_sym, robot_result)
|
|
197
|
-
.continue(robot_result)
|
|
198
|
-
|
|
199
|
-
# 5. Activate the appropriate optional task based on output.
|
|
200
|
-
# These symbols MUST match the task names declared in create_network.
|
|
201
|
-
category = robot_result.last_text_content.to_s.strip.downcase
|
|
202
|
-
case category
|
|
203
|
-
when /billing/ then new_result.activate(:billing_agent)
|
|
204
|
-
when /technical/ then new_result.activate(:tech_agent)
|
|
205
|
-
else new_result.activate(:general_agent)
|
|
206
|
-
end
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
> [!WARNING]
|
|
212
|
-
> `activate` validates its argument and **raises**. Activating `:billing` when the
|
|
213
|
-
> network declared `task :billing_agent, ...` aborts the run with
|
|
214
|
-
> `ArgumentError: Step :classifier attempted to activate unknown step :billing`.
|
|
215
|
-
> Activating a task that was *not* declared `depends_on: :optional` raises too:
|
|
216
|
-
> `ArgumentError: Step :classifier attempted to activate non-optional step :billing_agent.`
|
|
217
|
-
> Use `result.activated_steps` to inspect what routing actually did.
|
|
218
|
-
|
|
219
|
-
!!! note "extract_run_context"
|
|
220
|
-
The `extract_run_context(result)` method is a protected helper on `Robot`. It extracts `run_params` from the SimpleFlow result context, handles value propagation from previous steps, and separates robot-specific params (`mcp:`, `tools:`, `memory:`, `network_memory:`) from the message and other context.
|
|
221
|
-
|
|
222
|
-
## With Context Passing
|
|
223
|
-
|
|
224
|
-
Enhanced version where the classifier passes additional context to specialists:
|
|
225
|
-
|
|
226
|
-
```ruby
|
|
227
|
-
class ContextAwareClassifier < RobotLab::Robot
|
|
228
|
-
def call(result)
|
|
229
|
-
context = extract_run_context(result)
|
|
230
|
-
message = context.delete(:message)
|
|
231
|
-
robot_result = run(message, **context)
|
|
232
|
-
|
|
233
|
-
# Store classification and original message in context for specialist
|
|
234
|
-
new_result = result
|
|
235
|
-
.with_context(@name.to_sym, robot_result)
|
|
236
|
-
.with_context(:classification, robot_result.last_text_content.strip)
|
|
237
|
-
.with_context(:original_message, result.context[:run_params][:message])
|
|
238
|
-
.continue(robot_result)
|
|
239
|
-
|
|
240
|
-
category = robot_result.last_text_content.to_s.downcase
|
|
241
|
-
case category
|
|
242
|
-
when /billing/ then new_result.activate(:billing_agent)
|
|
243
|
-
when /technical/ then new_result.activate(:tech_agent)
|
|
244
|
-
else new_result.activate(:general_agent)
|
|
245
|
-
end
|
|
246
|
-
end
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
# Specialist can access shared context
|
|
250
|
-
class BillingAgent < RobotLab::Robot
|
|
251
|
-
def call(result)
|
|
252
|
-
# Access context from classifier
|
|
253
|
-
classification = result.context[:classification]
|
|
254
|
-
original_message = result.context[:original_message]
|
|
255
|
-
|
|
256
|
-
context = extract_run_context(result)
|
|
257
|
-
message = context.delete(:message)
|
|
258
|
-
|
|
259
|
-
robot_result = run(message, **context)
|
|
260
|
-
|
|
261
|
-
result.with_context(@name.to_sym, robot_result).continue(robot_result)
|
|
262
|
-
end
|
|
263
|
-
end
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
## Per-Task Configuration
|
|
267
|
-
|
|
268
|
-
Tasks can carry their own context, a tool allowlist, and MCP servers:
|
|
269
|
-
|
|
270
|
-
```ruby
|
|
271
|
-
# Tools must already be ATTACHED to the robot...
|
|
272
|
-
billing_agent = RobotLab.build(
|
|
273
|
-
name: "billing_agent",
|
|
274
|
-
system_prompt: "You handle billing.",
|
|
275
|
-
local_tools: [RefundTool, InvoiceTool, AuditTool]
|
|
276
|
-
)
|
|
277
|
-
|
|
278
|
-
network = RobotLab.create_network(name: "support") do
|
|
279
|
-
task :classifier, classifier, depends_on: :none
|
|
280
|
-
task :billing_agent, billing_agent,
|
|
281
|
-
context: { department: "billing", escalation_level: 2 },
|
|
282
|
-
tools: [RefundTool, InvoiceTool], # ...and this SELECTS from them
|
|
283
|
-
depends_on: :optional
|
|
284
|
-
task :tech_agent, tech_agent,
|
|
285
|
-
context: { department: "technical" },
|
|
286
|
-
mcp: [filesystem_server],
|
|
287
|
-
tools: :inherit, # send every discovered MCP tool
|
|
288
|
-
depends_on: :optional
|
|
289
|
-
end
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
> [!WARNING]
|
|
293
|
-
> Per-task `tools:` is an **allowlist over tools the robot already has** — it never
|
|
294
|
-
> attaches anything. Give a task `tools: [RefundTool]` for a robot whose
|
|
295
|
-
> `local_tools` are empty and it sends zero tools.
|
|
296
|
-
>
|
|
297
|
-
> Matching is by string comparison against each attached tool's `name`, so the two
|
|
298
|
-
> sides must agree. `local_tools: [RefundTool]` (the class) matches
|
|
299
|
-
> `tools: [RefundTool]`, but `local_tools: [RefundTool.new]` (an instance, whose
|
|
300
|
-
> name is `"refund"`) does not — use `tools: %w[refund]` there.
|
|
301
|
-
>
|
|
302
|
-
> Omitting `tools:` from a task means `:none`: the robot sends no tools at all.
|
|
303
|
-
> Use `tools: :inherit` for "send everything attached".
|
|
304
|
-
|
|
305
|
-
The `Task` wrapper deep-merges per-task context with the network's run params before delegating to the robot's `call` method.
|
|
306
|
-
|
|
307
|
-
## Pipeline Pattern
|
|
308
|
-
|
|
309
|
-
Sequential processing pipeline where each robot depends on the previous:
|
|
310
|
-
|
|
311
|
-
```ruby
|
|
312
|
-
extractor = RobotLab.build(
|
|
313
|
-
name: "extractor",
|
|
314
|
-
system_prompt: "Extract key information from documents."
|
|
315
|
-
)
|
|
316
|
-
|
|
317
|
-
analyzer = RobotLab.build(
|
|
318
|
-
name: "analyzer",
|
|
319
|
-
system_prompt: "Analyze extracted data and provide insights."
|
|
320
|
-
)
|
|
321
|
-
|
|
322
|
-
formatter = RobotLab.build(
|
|
323
|
-
name: "formatter",
|
|
324
|
-
system_prompt: "Format analysis results into a clear report."
|
|
325
|
-
)
|
|
326
|
-
|
|
327
|
-
network = RobotLab.create_network(name: "document_processor") do
|
|
328
|
-
task :extract, extractor, depends_on: :none
|
|
329
|
-
task :analyze, analyzer, depends_on: [:extract]
|
|
330
|
-
task :format, formatter, depends_on: [:analyze]
|
|
331
|
-
end
|
|
332
|
-
|
|
333
|
-
result = network.run(message: "Process this document")
|
|
334
|
-
puts result.value.last_text_content
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
## Parallel Analysis Pattern
|
|
338
|
-
|
|
339
|
-
Fan-out / fan-in pattern where multiple robots analyze in parallel and a synthesizer merges results:
|
|
340
|
-
|
|
341
|
-
```ruby
|
|
342
|
-
# Robot names deliberately match their task names -- result.context is keyed
|
|
343
|
-
# by the ROBOT's name, so mismatched names produce nil lookups below.
|
|
344
|
-
sentiment = RobotLab.build(name: "sentiment", system_prompt: "Score sentiment.")
|
|
345
|
-
entities = RobotLab.build(name: "entities", system_prompt: "Extract entities.")
|
|
346
|
-
keywords = RobotLab.build(name: "keywords", system_prompt: "Extract keywords.")
|
|
347
|
-
|
|
348
|
-
network = RobotLab.create_network(name: "multi_analysis") do
|
|
349
|
-
task :prepare, preparer, depends_on: :none
|
|
350
|
-
|
|
351
|
-
# These run in parallel (all depend on :prepare)
|
|
352
|
-
task :sentiment, sentiment, depends_on: [:prepare]
|
|
353
|
-
task :entities, entities, depends_on: [:prepare]
|
|
354
|
-
task :keywords, keywords, depends_on: [:prepare]
|
|
355
|
-
|
|
356
|
-
# Waits for all three to complete
|
|
357
|
-
task :summarize, summarizer, depends_on: [:sentiment, :entities, :keywords]
|
|
358
|
-
end
|
|
359
|
-
|
|
360
|
-
result = network.run(message: "Analyze this text")
|
|
361
|
-
|
|
362
|
-
# Access parallel results from context (keyed by robot name)
|
|
363
|
-
puts "Sentiment: #{result.context[:sentiment].last_text_content}"
|
|
364
|
-
puts "Entities: #{result.context[:entities].last_text_content}"
|
|
365
|
-
puts "Keywords: #{result.context[:keywords].last_text_content}"
|
|
366
|
-
puts "Summary: #{result.value.last_text_content}"
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
> [!WARNING]
|
|
370
|
-
> `result.context` is keyed by `@name` of the robot that wrote it, not by the task
|
|
371
|
-
> name. Had these robots kept names like `sentiment_analyzer`, the lookups above
|
|
372
|
-
> would return `nil` even though every task ran. Keep robot name and task name
|
|
373
|
-
> identical.
|
|
374
|
-
|
|
375
|
-
## Shared Memory in Networks
|
|
376
|
-
|
|
377
|
-
Networks provide a shared `Memory` instance that all robots can read and write. This is especially useful for parallel robots that need to coordinate:
|
|
378
|
-
|
|
379
|
-
```ruby
|
|
380
|
-
class AnalysisRobot < RobotLab::Robot
|
|
381
|
-
def initialize(memory_key:, **opts)
|
|
382
|
-
super(**opts)
|
|
383
|
-
@memory_key = memory_key
|
|
384
|
-
end
|
|
385
|
-
|
|
386
|
-
def call(result)
|
|
387
|
-
context = extract_run_context(result)
|
|
388
|
-
network_memory = context.delete(:network_memory)
|
|
389
|
-
message = context.delete(:message)
|
|
390
|
-
|
|
391
|
-
robot_result = run(message, network_memory: network_memory, **context)
|
|
392
|
-
|
|
393
|
-
# Write parsed results to shared memory
|
|
394
|
-
if network_memory
|
|
395
|
-
network_memory.current_writer = @name
|
|
396
|
-
network_memory.set(@memory_key, robot_result.last_text_content)
|
|
397
|
-
end
|
|
398
|
-
|
|
399
|
-
result.with_context(@name.to_sym, robot_result).continue(robot_result)
|
|
400
|
-
end
|
|
401
|
-
end
|
|
402
|
-
```
|
|
403
|
-
|
|
404
|
-
## Conditional Halting
|
|
405
|
-
|
|
406
|
-
Use `result.halt` to stop the pipeline early:
|
|
407
|
-
|
|
408
|
-
```ruby
|
|
409
|
-
class ValidatorRobot < RobotLab::Robot
|
|
410
|
-
def call(result)
|
|
411
|
-
context = extract_run_context(result)
|
|
412
|
-
message = context.delete(:message)
|
|
413
|
-
robot_result = run(message, **context)
|
|
414
|
-
|
|
415
|
-
if robot_result.last_text_content.include?("INVALID")
|
|
416
|
-
# Halt the pipeline early
|
|
417
|
-
result.halt(robot_result)
|
|
418
|
-
else
|
|
419
|
-
result.with_context(@name.to_sym, robot_result).continue(robot_result)
|
|
420
|
-
end
|
|
421
|
-
end
|
|
422
|
-
end
|
|
423
|
-
|
|
424
|
-
network = RobotLab.create_network(name: "validated_pipeline") do
|
|
425
|
-
task :validate, validator, depends_on: :none
|
|
426
|
-
task :process, processor, depends_on: [:validate] # Only runs if not halted
|
|
427
|
-
end
|
|
428
|
-
|
|
429
|
-
result = network.run(message: "Process this")
|
|
430
|
-
if result.continue?
|
|
431
|
-
puts "Processing complete: #{result.value.last_text_content}"
|
|
432
|
-
else
|
|
433
|
-
puts "Validation failed: #{result.value.last_text_content}"
|
|
434
|
-
end
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
> [!WARNING]
|
|
438
|
-
> `SimpleFlow::Result` has **no** `halted?` or `continued?` predicate — calling
|
|
439
|
-
> either raises `NoMethodError`. The predicate is `continue?`, which returns
|
|
440
|
-
> `false` after `halt`. The full public API is exactly `activate`,
|
|
441
|
-
> `activated_steps`, `context`, `continue`, `continue?`, `errors`, `halt`, `value`,
|
|
442
|
-
> `with_context`, and `with_error`. `with_value` is **private** — calling it raises
|
|
443
|
-
> `NoMethodError`; use `continue(value)` or `halt(value)` to set the value.
|
|
444
|
-
|
|
445
|
-
## Running
|
|
446
|
-
|
|
447
|
-
```bash
|
|
448
|
-
export ANTHROPIC_API_KEY="your-key"
|
|
449
|
-
|
|
450
|
-
# Classifier + three specialists with optional-task routing
|
|
451
|
-
ruby examples/03_network.rb
|
|
452
|
-
|
|
453
|
-
# Shared reactive memory across concurrent robots
|
|
454
|
-
ruby examples/07_network_memory.rb
|
|
455
|
-
|
|
456
|
-
# Network visualization and introspection (no LLM calls)
|
|
457
|
-
ruby examples/11_network_introspection.rb
|
|
458
|
-
```
|
|
459
|
-
|
|
460
|
-
## Key Concepts
|
|
461
|
-
|
|
462
|
-
1. **SimpleFlow Pipeline**: DAG-based execution with dependency management via `depends_on:`. The DSL method is `task` — there is no `step`, no `router:` kwarg, and no `Router` class
|
|
463
|
-
2. **Optional Tasks**: Use `depends_on: :optional` for tasks activated dynamically by classifiers
|
|
464
|
-
3. **Robot#call Override**: Routing is written in Ruby — subclass `Robot`, override `call`, and call `result.activate(:task_name)`
|
|
465
|
-
4. **extract_run_context**: Helper method to extract message and params from `SimpleFlow::Result`
|
|
466
|
-
5. **Context Flow**: `result.context` is keyed by the **robot's** name (`with_context(@name.to_sym, ...)`), while `activate` takes a **task** name — keep the two identical
|
|
467
|
-
6. **Result predicates**: `continue?` (not `halted?`), plus `activated_steps` to see what routing actually did
|
|
468
|
-
7. **Parallel Execution**: Tasks with the same dependencies run concurrently; cap in-flight LLM calls with `RunConfig.new(max_concurrent_robots: N)`
|
|
469
|
-
8. **Shared Memory**: Network memory (`network_memory:`) enables inter-robot communication
|
|
470
|
-
9. **Per-Task Configuration**: Each task can carry its own context, MCP servers, and a `tools:` allowlist over the robot's already-attached tools
|
|
471
|
-
|
|
472
|
-
## See Also
|
|
473
|
-
|
|
474
|
-
- [Creating Networks Guide](../guides/creating-networks.md)
|
|
475
|
-
- [Network Orchestration](../architecture/network-orchestration.md)
|
|
476
|
-
- [API Reference: Network](../api/core/network.md)
|