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,478 +0,0 @@
|
|
|
1
|
-
# Network Orchestration
|
|
2
|
-
|
|
3
|
-
Networks coordinate multiple robots using [SimpleFlow](https://github.com/MadBomber/simple_flow) pipelines for DAG-based execution.
|
|
4
|
-
|
|
5
|
-
## Network Structure
|
|
6
|
-
|
|
7
|
-
A network is a thin wrapper around `SimpleFlow::Pipeline`:
|
|
8
|
-
|
|
9
|
-
- **Pipeline**: DAG-based execution engine
|
|
10
|
-
- **Robots**: Named collection of robot instances
|
|
11
|
-
- **Tasks**: Wrap robots with per-task configuration and define dependencies
|
|
12
|
-
- **Memory**: Shared reactive memory for inter-robot communication
|
|
13
|
-
|
|
14
|
-
```ruby
|
|
15
|
-
network = RobotLab.create_network(name: "customer_service") do
|
|
16
|
-
task :classifier, classifier_robot, depends_on: :none
|
|
17
|
-
task :billing, billing_robot, depends_on: :optional
|
|
18
|
-
task :technical, technical_robot, depends_on: :optional
|
|
19
|
-
end
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## Runnable Protocol
|
|
23
|
-
|
|
24
|
-
`Network` implements `RobotLab::Runnable` — the same interface `Robot` implements — so code that needs to run "a robot or a network" doesn't have to branch on `is_a?(RobotLab::Network)`. For a `Network`: `crew` returns `robots.values` (pipeline order), `chief` is `crew.first`, `robot_count` is `crew.size`, and `network?` is `true`. `run(message = nil, **opts)` accepts a positional message the same way `Robot#run` does — it's folded into `message:` — while the existing `run(message: ...)` keyword form still works unchanged. See [Runnable Protocol](../architecture/core-concepts.md#runnable-protocol) in Core Concepts for the full comparison against `Robot`.
|
|
25
|
-
|
|
26
|
-
## Creating Networks
|
|
27
|
-
|
|
28
|
-
Networks are created via `RobotLab.create_network` with a block DSL:
|
|
29
|
-
|
|
30
|
-
```ruby
|
|
31
|
-
analyst = RobotLab.build(name: "analyst", system_prompt: "Analyze the input.")
|
|
32
|
-
writer = RobotLab.build(name: "writer", system_prompt: "Write a report.")
|
|
33
|
-
reviewer = RobotLab.build(name: "reviewer", system_prompt: "Review the report.")
|
|
34
|
-
|
|
35
|
-
network = RobotLab.create_network(name: "pipeline") do
|
|
36
|
-
task :analyst, analyst, depends_on: :none
|
|
37
|
-
task :writer, writer, depends_on: [:analyst]
|
|
38
|
-
task :reviewer, reviewer, depends_on: [:writer]
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
result = network.run(message: "Analyze this quarterly data")
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
## Task Configuration
|
|
45
|
-
|
|
46
|
-
Tasks can have per-task configuration that is deep-merged with network run params:
|
|
47
|
-
|
|
48
|
-
```ruby
|
|
49
|
-
# The allowlist below matches because the tool was attached as a CLASS.
|
|
50
|
-
billing_robot = RobotLab.build(name: "billing", system_prompt: "...",
|
|
51
|
-
local_tools: [RefundTool])
|
|
52
|
-
|
|
53
|
-
network = RobotLab.create_network(name: "support") do
|
|
54
|
-
task :classifier, classifier_robot, depends_on: :none
|
|
55
|
-
task :billing, billing_robot,
|
|
56
|
-
context: { department: "billing", escalation_level: 2 },
|
|
57
|
-
tools: [RefundTool],
|
|
58
|
-
depends_on: :optional
|
|
59
|
-
task :technical, technical_robot,
|
|
60
|
-
context: { department: "technical" },
|
|
61
|
-
mcp: [filesystem_server],
|
|
62
|
-
depends_on: :optional
|
|
63
|
-
end
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
!!! warning "An explicit `tools:` array must match the attachment form"
|
|
67
|
-
`ToolConfig.filter_tools` selects with `allowed_set.include?(tool_name(tool))`,
|
|
68
|
-
and `tool_name` is just `tool.name.to_s`. But `Class#name` and
|
|
69
|
-
`RubyLLM::Tool#name` return different strings, so the entry you list has to
|
|
70
|
-
match how the tool was attached:
|
|
71
|
-
|
|
72
|
-
| Attached as | Allowlist entry | Result |
|
|
73
|
-
|-------------|-----------------|--------|
|
|
74
|
-
| `local_tools: [RefundTool]` (class) | `tools: [RefundTool]` → `"RefundTool"` | matches |
|
|
75
|
-
| `local_tools: [RefundTool]` (class) | `tools: %w[refund]` | no match |
|
|
76
|
-
| `local_tools: [RefundTool.new]` (instance) | `tools: [RefundTool]` | no match |
|
|
77
|
-
| `local_tools: [RefundTool.new]` (instance) | `tools: %w[refund]` | matches |
|
|
78
|
-
|
|
79
|
-
Both forms work; mixing them silently yields an empty tool list. Pick one
|
|
80
|
-
convention per robot and keep the allowlist in the same form.
|
|
81
|
-
|
|
82
|
-
A task's `tools:` is *not* validated — it lands in `run_params` and reaches
|
|
83
|
-
the robot as a runtime value. The robot **constructor**'s `tools:` kwarg is
|
|
84
|
-
validated by `validate_tools_filter!` and raises `ArgumentError` for
|
|
85
|
-
anything that is not a String or Symbol, so the class form shown above is
|
|
86
|
-
usable only at the task/`run` level.
|
|
87
|
-
|
|
88
|
-
### Task Parameters
|
|
89
|
-
|
|
90
|
-
| Parameter | Type | Description |
|
|
91
|
-
|-----------|------|-------------|
|
|
92
|
-
| `name` | Symbol | Task/step name |
|
|
93
|
-
| `robot` | Robot | The robot instance |
|
|
94
|
-
| `context` | Hash | Task-specific context (deep-merged with run params) |
|
|
95
|
-
| `mcp` | Symbol, Array | MCP server config (`:none`, `:inherit`, or array) |
|
|
96
|
-
| `tools` | Symbol, Array | Tools config (`:none`, `:inherit`, or array) |
|
|
97
|
-
| `memory` | Memory, Hash, nil | Task-specific memory |
|
|
98
|
-
| `config` | RunConfig, nil | Per-task RunConfig, merged into the network config the robot sees (`mcp`/`tools` only in practice) |
|
|
99
|
-
| `depends_on` | Symbol, Array | Dependencies (`:none`, `:optional`, or task names) |
|
|
100
|
-
| `poller_group` | Symbol | Bus poller group label for this robot (default `:default`; purely organizational) |
|
|
101
|
-
|
|
102
|
-
`mcp:` and `tools:` default to `:none` here. Anything other than `:none` is written into `run_params` and reaches the robot as its **runtime** value, so it is resolved against the robot's build-time config exactly as if it had been passed to `run`.
|
|
103
|
-
|
|
104
|
-
## Execution Model
|
|
105
|
-
|
|
106
|
-
```mermaid
|
|
107
|
-
stateDiagram-v2
|
|
108
|
-
[*] --> Start
|
|
109
|
-
Start --> ExecuteTask: next ready task
|
|
110
|
-
ExecuteTask --> CheckDependents: task complete
|
|
111
|
-
CheckDependents --> ExecuteTask: more tasks ready
|
|
112
|
-
CheckDependents --> Complete: all tasks done
|
|
113
|
-
ExecuteTask --> Halted: task halts
|
|
114
|
-
Complete --> [*]
|
|
115
|
-
Halted --> [*]
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
### Task Dependency Types
|
|
119
|
-
|
|
120
|
-
| Type | Description |
|
|
121
|
-
|------|-------------|
|
|
122
|
-
| `:none` | No dependencies, runs first |
|
|
123
|
-
| `[:task1, :task2]` | Waits for listed tasks to complete |
|
|
124
|
-
| `:optional` | Only runs when explicitly activated |
|
|
125
|
-
|
|
126
|
-
## Robot#call Interface
|
|
127
|
-
|
|
128
|
-
Each robot implements the SimpleFlow step interface via `call(result)`:
|
|
129
|
-
|
|
130
|
-
```ruby
|
|
131
|
-
# Inside Robot (simplified -- timing and the rescue are elided)
|
|
132
|
-
def call(result)
|
|
133
|
-
run_context = extract_run_context(result)
|
|
134
|
-
message = run_context.delete(:message)
|
|
135
|
-
|
|
136
|
-
robot_result = run(message, **run_context)
|
|
137
|
-
robot_result.duration = ... # monotonic elapsed seconds
|
|
138
|
-
|
|
139
|
-
result
|
|
140
|
-
.with_context(@name.to_sym, robot_result) # keyed by the ROBOT's name
|
|
141
|
-
.continue(robot_result)
|
|
142
|
-
end
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
The real method also wraps the body so that any exception — including non-`StandardError` ones — is turned into a `RobotResult` whose text is `"Error: <class>: <message>"`, so one failing robot does not crash the pipeline.
|
|
146
|
-
|
|
147
|
-
### extract_run_context
|
|
148
|
-
|
|
149
|
-
The `extract_run_context` method pulls parameters from the SimpleFlow result:
|
|
150
|
-
|
|
151
|
-
- Deletes `:mcp`, `:tools`, `:memory`, `:network_memory`, `:network_config`, `:network`, and `:task` out of `run_params`, then re-attaches them as explicit keyword arguments to `run`
|
|
152
|
-
- `:mcp` and `:tools` default to `:none` when the task did not set them — matching `run`'s own defaults
|
|
153
|
-
- Merges the current result value into the remaining context
|
|
154
|
-
- If the previous result value is a `RobotResult`, uses its `last_text_content` as the message
|
|
155
|
-
- If it is a String, uses it directly as the message
|
|
156
|
-
- If it is a Hash, merges it into the context
|
|
157
|
-
- Anything else is coerced with `to_s` and used as the message
|
|
158
|
-
|
|
159
|
-
## Task#call Interface
|
|
160
|
-
|
|
161
|
-
Each `Task` wraps a robot and enhances the SimpleFlow result before delegation:
|
|
162
|
-
|
|
163
|
-
```ruby
|
|
164
|
-
# Inside Task (simplified)
|
|
165
|
-
def call(result)
|
|
166
|
-
context = TaskHookContext.new(network: @network, task: self, robot: @robot,
|
|
167
|
-
memory: @memory || @network&.memory, config: @config)
|
|
168
|
-
|
|
169
|
-
RobotLab::Hooks.run(:task, context, registries: [RobotLab.hooks, @network&.hooks]) do
|
|
170
|
-
@robot.call(enhanced_result(result))
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
def enhanced_result(result)
|
|
175
|
-
run_params = deep_merge(result.context[:run_params] || {}, @context)
|
|
176
|
-
|
|
177
|
-
run_params[:mcp] = @mcp unless @mcp == :none
|
|
178
|
-
run_params[:tools] = @tools unless @tools == :none
|
|
179
|
-
run_params[:memory] = @memory if @memory
|
|
180
|
-
|
|
181
|
-
# Back-references the robot needs for hooks and config resolution
|
|
182
|
-
run_params[:task] = self
|
|
183
|
-
run_params[:network] = @network if @network
|
|
184
|
-
|
|
185
|
-
if @config
|
|
186
|
-
network_rc = run_params[:network_config]
|
|
187
|
-
run_params[:network_config] = network_rc ? network_rc.merge(@config) : @config
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
result.with_context(:run_params, run_params)
|
|
191
|
-
end
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
Two things to note. The `:task` hook family is dispatched against `[RobotLab.hooks, network&.hooks]` only — a handler registered with `robot.on` never fires for task hooks. And the task's own `config:` is merged into `run_params[:network_config]`, which is the value `Robot#resolve_mcp_hierarchy` / `#resolve_tools_hierarchy` consult as the parent level.
|
|
195
|
-
|
|
196
|
-
## SimpleFlow::Result
|
|
197
|
-
|
|
198
|
-
The result object flows through the pipeline:
|
|
199
|
-
|
|
200
|
-
```ruby
|
|
201
|
-
result.value # Current task's output (RobotResult)
|
|
202
|
-
result.context # Accumulated context from all tasks
|
|
203
|
-
result.continue? # Whether execution continues (the only status predicate)
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
### Result Methods
|
|
207
|
-
|
|
208
|
-
This is the complete public API of `SimpleFlow::Result` (simple_flow 0.4):
|
|
209
|
-
|
|
210
|
-
| Method | Description |
|
|
211
|
-
|--------|-------------|
|
|
212
|
-
| `value` | Current value flowing through the pipeline |
|
|
213
|
-
| `context` | Accumulated context hash |
|
|
214
|
-
| `continue(value)` | Continue to next tasks |
|
|
215
|
-
| `continue?` | Whether the pipeline is still continuing |
|
|
216
|
-
| `halt(value)` | Stop pipeline execution |
|
|
217
|
-
| `with_context(key, val)` | Add data to context |
|
|
218
|
-
| `with_error(key, message)` | Record an error (both arguments required) |
|
|
219
|
-
| `errors` | Recorded errors |
|
|
220
|
-
| `activate(task_name)` | Enable an optional task |
|
|
221
|
-
| `activated_steps` | Optional tasks that have been activated |
|
|
222
|
-
|
|
223
|
-
There is no `halted?`, no `continued?`, and no `with_value` — use `continue?` for status and `continue(value)` to set a new value.
|
|
224
|
-
|
|
225
|
-
### Context Structure
|
|
226
|
-
|
|
227
|
-
```ruby
|
|
228
|
-
{
|
|
229
|
-
run_params: { message: "...", customer_id: 123,
|
|
230
|
-
network_memory: memory, network: network, task: task },
|
|
231
|
-
classifier: RobotResult, # Stored by Robot#call under the ROBOT's name
|
|
232
|
-
billing: RobotResult,
|
|
233
|
-
# ... other robot results
|
|
234
|
-
}
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
`Robot#call` stores its output with `result.with_context(@name.to_sym, robot_result)` — the key is the **robot's** `name`, not the task name. The two coincide only when you name them identically:
|
|
238
|
-
|
|
239
|
-
```ruby
|
|
240
|
-
worker = RobotLab.build(name: "worker_bot", system_prompt: "...")
|
|
241
|
-
|
|
242
|
-
net = RobotLab.create_network(name: "n") do
|
|
243
|
-
task :analysis, worker, depends_on: :none
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
res = net.run(message: "hi")
|
|
247
|
-
res.context.keys #=> [:run_params, :worker_bot] -- not :analysis
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
## Optional Task Activation
|
|
251
|
-
|
|
252
|
-
Optional tasks (those with `depends_on: :optional`) do not run automatically. They must be activated by a preceding task using `result.activate(:task_name)`.
|
|
253
|
-
|
|
254
|
-
This pattern is commonly used with a classifier robot that analyzes the input and routes to the appropriate handler:
|
|
255
|
-
|
|
256
|
-
```ruby
|
|
257
|
-
classifier = RobotLab.build(
|
|
258
|
-
name: "classifier",
|
|
259
|
-
system_prompt: "Classify the request. Respond with: BILLING, TECHNICAL, or GENERAL."
|
|
260
|
-
)
|
|
261
|
-
|
|
262
|
-
billing = RobotLab.build(name: "billing", system_prompt: "Handle billing requests.")
|
|
263
|
-
technical = RobotLab.build(name: "technical", system_prompt: "Handle technical requests.")
|
|
264
|
-
general = RobotLab.build(name: "general", system_prompt: "Handle general requests.")
|
|
265
|
-
|
|
266
|
-
network = RobotLab.create_network(name: "support") do
|
|
267
|
-
task :classifier, classifier, depends_on: :none
|
|
268
|
-
task :billing, billing, depends_on: :optional
|
|
269
|
-
task :technical, technical, depends_on: :optional
|
|
270
|
-
task :general, general, depends_on: :optional
|
|
271
|
-
end
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
### Classifier Robot Pattern
|
|
275
|
-
|
|
276
|
-
To activate optional tasks, a robot subclass overrides `call` to inspect its own output and activate the appropriate downstream task:
|
|
277
|
-
|
|
278
|
-
```ruby
|
|
279
|
-
class ClassifierRobot < RobotLab::Robot
|
|
280
|
-
def call(result)
|
|
281
|
-
run_context = extract_run_context(result)
|
|
282
|
-
message = run_context.delete(:message)
|
|
283
|
-
|
|
284
|
-
robot_result = run(message, **run_context)
|
|
285
|
-
|
|
286
|
-
new_result = result
|
|
287
|
-
.with_context(@name.to_sym, robot_result)
|
|
288
|
-
.continue(robot_result)
|
|
289
|
-
|
|
290
|
-
# Analyze output and activate the appropriate optional task
|
|
291
|
-
category = robot_result.last_text_content.to_s.downcase
|
|
292
|
-
|
|
293
|
-
case category
|
|
294
|
-
when /billing/
|
|
295
|
-
new_result.activate(:billing)
|
|
296
|
-
when /technical/
|
|
297
|
-
new_result.activate(:technical)
|
|
298
|
-
else
|
|
299
|
-
new_result.activate(:general)
|
|
300
|
-
end
|
|
301
|
-
end
|
|
302
|
-
end
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
## Shared Memory
|
|
306
|
-
|
|
307
|
-
All robots in a network share the network's memory during execution. The network injects its memory into the run context:
|
|
308
|
-
|
|
309
|
-
```ruby
|
|
310
|
-
# Inside Network#run
|
|
311
|
-
def run(message = nil, **run_context)
|
|
312
|
-
run_context[:message] = message unless message.nil? # Runnable protocol
|
|
313
|
-
|
|
314
|
-
run_context[:network_memory] = @memory
|
|
315
|
-
run_context[:network] = self
|
|
316
|
-
run_context[:network_config] = @config unless @config.empty?
|
|
317
|
-
|
|
318
|
-
context = NetworkRunHookContext.new(network: self, context: run_context,
|
|
319
|
-
memory: @memory, config: @config)
|
|
320
|
-
|
|
321
|
-
RobotLab::Hooks.run(:network_run, context, registries: [RobotLab.hooks, @hooks]) do
|
|
322
|
-
if @parallel_mode == :ractor
|
|
323
|
-
run_with_ractor_scheduler(context.context)
|
|
324
|
-
else
|
|
325
|
-
initial_result = SimpleFlow::Result.new(
|
|
326
|
-
context.context,
|
|
327
|
-
context: { run_params: context.context }
|
|
328
|
-
)
|
|
329
|
-
@pipeline.call_parallel(initial_result, max_concurrent: @config.max_concurrent_robots)
|
|
330
|
-
end
|
|
331
|
-
end
|
|
332
|
-
end
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
Beyond injecting the shared memory, this does four things worth knowing:
|
|
336
|
-
|
|
337
|
-
- It puts the network itself and (when non-empty) the network's `RunConfig` into `run_params`, which is how robots find the parent level for `mcp`/`tools` resolution.
|
|
338
|
-
- The whole run is wrapped in the `:network_run` hook, dispatched against `[RobotLab.hooks, network.hooks]`.
|
|
339
|
-
- `parallel_mode: :ractor` routes to `run_with_ractor_scheduler` instead of the SimpleFlow pipeline; it raises `RobotLab::DependencyError` unless the `robot_lab-ractor` gem is loaded.
|
|
340
|
-
- `max_concurrent:` comes from `@config.max_concurrent_robots` — the one `RunConfig` field the network itself consumes.
|
|
341
|
-
|
|
342
|
-
Robots use the shared memory for inter-robot communication:
|
|
343
|
-
|
|
344
|
-
```ruby
|
|
345
|
-
# Robot A writes to shared memory
|
|
346
|
-
memory.set(:classification, "billing")
|
|
347
|
-
|
|
348
|
-
# Robot B reads from shared memory
|
|
349
|
-
category = memory.get(:classification, wait: true)
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
See [Memory Management](state-management.md) for details on reactive features like subscriptions and blocking reads.
|
|
353
|
-
|
|
354
|
-
## Broadcasting
|
|
355
|
-
|
|
356
|
-
Networks support a broadcast channel for network-wide announcements:
|
|
357
|
-
|
|
358
|
-
```ruby
|
|
359
|
-
# Register handlers
|
|
360
|
-
network.on_broadcast do |message|
|
|
361
|
-
case message[:payload][:event]
|
|
362
|
-
when :pause
|
|
363
|
-
pause_current_work
|
|
364
|
-
when :resume
|
|
365
|
-
resume_work
|
|
366
|
-
end
|
|
367
|
-
end
|
|
368
|
-
|
|
369
|
-
# Send broadcasts
|
|
370
|
-
network.broadcast(event: :pause, reason: "rate limit hit")
|
|
371
|
-
network.broadcast(event: :phase_complete, phase: "analysis")
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
Each handler is invoked inside an `Async { }` block, and the message is also written to memory at the `_network_broadcast` key (`Network::BROADCAST_KEY`), so robots can subscribe via `memory.subscribe(:_network_broadcast)`. Outside a running reactor, `Async { }` runs the block synchronously on the caller's thread. The message handed to a handler is `{ payload:, network:, timestamp: }`, which is why the examples above reach for `message[:payload][:event]`.
|
|
375
|
-
|
|
376
|
-
## Parallel Execution
|
|
377
|
-
|
|
378
|
-
Tasks with the same dependencies can run in parallel:
|
|
379
|
-
|
|
380
|
-
```ruby
|
|
381
|
-
network = RobotLab.create_network(name: "analysis", concurrency: :threads) do
|
|
382
|
-
task :fetch, fetcher, depends_on: :none
|
|
383
|
-
|
|
384
|
-
# These three run in parallel after :fetch completes
|
|
385
|
-
task :sentiment, sentiment_bot, depends_on: [:fetch]
|
|
386
|
-
task :entities, entity_bot, depends_on: [:fetch]
|
|
387
|
-
task :keywords, keyword_bot, depends_on: [:fetch]
|
|
388
|
-
|
|
389
|
-
# Waits for all three
|
|
390
|
-
task :merge, merger, depends_on: [:sentiment, :entities, :keywords]
|
|
391
|
-
end
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
### Concurrency Modes
|
|
395
|
-
|
|
396
|
-
`create_network`'s `concurrency:` is passed straight to `SimpleFlow::Pipeline`:
|
|
397
|
-
|
|
398
|
-
| Mode | Description |
|
|
399
|
-
|------|-------------|
|
|
400
|
-
| `:auto` | SimpleFlow chooses best mode (default) |
|
|
401
|
-
| `:threads` | Use Ruby threads |
|
|
402
|
-
| `:async` | Use async/fiber |
|
|
403
|
-
|
|
404
|
-
The number of tasks running at once is capped by the network config's `max_concurrent_robots`, which `Network#run` passes to `call_parallel` as `max_concurrent:`.
|
|
405
|
-
|
|
406
|
-
Separately, `Network.new(parallel_mode:)` selects the execution backend. It defaults to `:async` (the SimpleFlow pipeline above). Setting `parallel_mode: :ractor` bypasses SimpleFlow entirely for a Ractor-based scheduler and requires the `robot_lab-ractor` gem — without it, `run` raises `RobotLab::DependencyError`.
|
|
407
|
-
|
|
408
|
-
## Data Flow
|
|
409
|
-
|
|
410
|
-
1. **Initial Value**: `network.run(message, **params)` creates an initial `SimpleFlow::Result` with the run context
|
|
411
|
-
2. **Run Params**: Stored in `result.context[:run_params]`, including `network_memory`, `network`, and (per task) `task`
|
|
412
|
-
3. **Task Results**: Each robot adds its `RobotResult` to context under **its own `name`**
|
|
413
|
-
4. **Final Value**: Last task's output becomes `result.value`
|
|
414
|
-
|
|
415
|
-
```ruby
|
|
416
|
-
result = network.run(
|
|
417
|
-
message: "Help with billing",
|
|
418
|
-
customer_id: 123
|
|
419
|
-
)
|
|
420
|
-
|
|
421
|
-
result.context[:run_params] #=> { message: "...", customer_id: 123,
|
|
422
|
-
# network_memory: ..., network: ..., task: ... }
|
|
423
|
-
result.context[:classifier] #=> RobotResult from the robot NAMED "classifier"
|
|
424
|
-
result.context[:billing] #=> RobotResult from the robot NAMED "billing"
|
|
425
|
-
result.value #=> Final RobotResult
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
If a robot's `name:` differs from the task name it was registered under, look it up by the robot's name.
|
|
429
|
-
|
|
430
|
-
## Visualization
|
|
431
|
-
|
|
432
|
-
Networks provide visualization methods via the underlying SimpleFlow pipeline:
|
|
433
|
-
|
|
434
|
-
```ruby
|
|
435
|
-
# ASCII representation
|
|
436
|
-
puts network.visualize
|
|
437
|
-
|
|
438
|
-
# Mermaid diagram
|
|
439
|
-
puts network.to_mermaid
|
|
440
|
-
|
|
441
|
-
# DOT format (Graphviz)
|
|
442
|
-
puts network.to_dot
|
|
443
|
-
|
|
444
|
-
# Execution plan description
|
|
445
|
-
puts network.execution_plan
|
|
446
|
-
```
|
|
447
|
-
|
|
448
|
-
## Network Inspection
|
|
449
|
-
|
|
450
|
-
```ruby
|
|
451
|
-
# Get a robot by its registration key
|
|
452
|
-
network.robot(:classifier) #=> Robot
|
|
453
|
-
network[:classifier] #=> Robot (alias)
|
|
454
|
-
|
|
455
|
-
# List all robots
|
|
456
|
-
network.available_robots #=> [Robot, Robot, ...]
|
|
457
|
-
network.crew #=> same array, via the Runnable protocol
|
|
458
|
-
|
|
459
|
-
# Add a robot without a task
|
|
460
|
-
network.add_robot(extra_robot)
|
|
461
|
-
|
|
462
|
-
# Remove a dynamically-added robot (returns it, or nil if absent).
|
|
463
|
-
# Only affects the crew (@robots) -- does not rewrite the pipeline.
|
|
464
|
-
network.remove_robot(:extra_robot)
|
|
465
|
-
|
|
466
|
-
# Convert to hash
|
|
467
|
-
network.to_h
|
|
468
|
-
#=> { name: "support", robots: ["classifier", "billing"],
|
|
469
|
-
# tasks: ["classifier", "billing"], optional_tasks: [],
|
|
470
|
-
# config: { ... } } # :config present only when the network config is non-empty
|
|
471
|
-
```
|
|
472
|
-
|
|
473
|
-
The `@robots` hash is keyed **by task name** for robots registered with `task`, but **by `robot.name`** for robots added with `add_robot`. So for a robot built as `name: "worker_bot"` and registered as `task :analysis, worker`, `network.robot(:analysis)` returns it and `network.robot(:worker_bot)` returns `nil` — the opposite of how `result.context` is keyed. `to_h[:robots]` therefore lists task names for task-registered robots.
|
|
474
|
-
|
|
475
|
-
## Next Steps
|
|
476
|
-
|
|
477
|
-
- [Memory Management](state-management.md) - Shared memory and reactive features
|
|
478
|
-
- [Message Flow](message-flow.md) - How messages are processed within robots
|