robot_lab 0.2.1 → 0.2.7
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/.envrc +1 -0
- data/.loki +60 -0
- data/.quality/reek_baseline.txt +43 -0
- data/.rubocop.yml +5 -167
- data/CHANGELOG.md +54 -0
- data/README.md +185 -64
- data/Rakefile +28 -12
- data/docs/api/core/index.md +41 -14
- data/docs/api/core/memory.md +247 -29
- data/docs/api/core/network.md +285 -33
- data/docs/api/core/result.md +120 -32
- data/docs/api/core/robot.md +636 -60
- data/docs/api/core/state.md +87 -197
- data/docs/api/core/tool.md +165 -20
- data/docs/api/errors.md +152 -14
- data/docs/api/hooks.md +469 -0
- data/docs/api/index.md +83 -7
- data/docs/api/mcp/client.md +129 -35
- data/docs/api/mcp/index.md +164 -23
- data/docs/api/mcp/server.md +27 -3
- data/docs/api/mcp/transports.md +94 -22
- data/docs/api/messages/index.md +26 -3
- data/docs/api/messages/text-message.md +33 -11
- data/docs/api/messages/tool-call-message.md +27 -4
- data/docs/api/messages/tool-result-message.md +23 -4
- data/docs/api/messages/user-message.md +45 -8
- data/docs/api/skills.md +519 -0
- data/docs/api/streaming/context.md +28 -5
- data/docs/api/streaming/index.md +57 -11
- data/docs/api/support.md +846 -0
- data/docs/architecture/core-concepts.md +118 -30
- data/docs/architecture/index.md +86 -11
- data/docs/architecture/message-flow.md +66 -29
- data/docs/architecture/network-orchestration.md +153 -38
- data/docs/architecture/robot-execution.md +173 -91
- data/docs/architecture/state-management.md +31 -12
- data/docs/concepts.md +176 -21
- data/docs/examples/basic-chat.md +72 -19
- data/docs/examples/index.md +117 -31
- data/docs/examples/mcp-server.md +154 -45
- data/docs/examples/multi-robot-network.md +91 -21
- data/docs/examples/tool-usage.md +104 -37
- data/docs/getting-started/configuration.md +299 -93
- data/docs/getting-started/installation.md +53 -41
- data/docs/getting-started/quick-start.md +51 -6
- data/docs/guides/building-robots.md +296 -50
- data/docs/guides/creating-networks.md +220 -31
- data/docs/guides/hooks.md +1084 -0
- data/docs/guides/index.md +5 -0
- data/docs/guides/knowledge.md +37 -6
- data/docs/guides/mcp-integration.md +211 -44
- data/docs/guides/memory.md +103 -12
- data/docs/guides/observability.md +166 -51
- data/docs/guides/streaming.md +184 -125
- data/docs/guides/using-tools.md +300 -11
- data/docs/index.md +37 -5
- data/examples/01_simple_robot.rb +2 -2
- data/examples/02_tools.rb +14 -4
- data/examples/03_network.rb +23 -9
- data/examples/04_mcp.rb +11 -4
- data/examples/05_streaming.rb +8 -5
- data/examples/06_prompt_templates.rb +13 -9
- data/examples/07_network_memory.rb +5 -5
- data/examples/08_llm_config.rb +20 -15
- data/examples/09_chaining.rb +4 -4
- data/examples/11_network_introspection.rb +4 -4
- data/examples/12_message_bus.rb +2 -2
- data/examples/13_spawn.rb +2 -2
- data/examples/14_rusty_circuit/README.md +1 -0
- data/examples/14_rusty_circuit/comic.rb +7 -3
- data/examples/14_rusty_circuit/display.rb +14 -0
- data/examples/14_rusty_circuit/heckler.rb +8 -6
- data/examples/14_rusty_circuit/open_mic.rb +17 -6
- data/examples/14_rusty_circuit/scout.rb +17 -10
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +14 -10
- data/examples/15_memory_network_and_bus/linux_writer.rb +2 -2
- data/examples/15_memory_network_and_bus/os_editor.rb +3 -1
- data/examples/15_memory_network_and_bus/os_writer.rb +4 -1
- data/examples/16_writers_room/writer.rb +22 -22
- data/examples/16_writers_room/writers_room.rb +2 -0
- data/examples/17_skills.rb +14 -13
- data/examples/18_rails/README.md +20 -1
- data/examples/18_rails/app/controllers/chat_controller.rb +5 -1
- data/examples/18_rails/app/jobs/robot_run_job.rb +11 -5
- data/examples/18_rails/app/robots/chat_robot.rb +11 -0
- data/examples/18_rails/config/initializers/robot_lab.rb +8 -0
- data/examples/19_token_tracking.rb +25 -9
- data/examples/20_circuit_breaker.rb +10 -7
- data/examples/21_learning_loop.rb +42 -16
- data/examples/22_context_compression.rb +23 -23
- data/examples/23_convergence.rb +24 -17
- data/examples/24_structured_delegation.rb +13 -8
- data/examples/25_history_search.rb +12 -8
- data/examples/27_incident_response/incident_response.rb +31 -13
- data/examples/28_mcp_discovery.rb +17 -13
- data/examples/29_ractor_tools.rb +4 -2
- data/examples/30_ractor_network.rb +22 -17
- data/examples/31_launch_assessment.rb +20 -9
- data/examples/32_newsletter_reader.rb +7 -2
- data/examples/33_stock_predictor.rb +34 -13
- data/examples/34_agentskills.rb +7 -3
- data/examples/35_hooks.rb +266 -0
- data/examples/README.md +203 -42
- data/examples/common.rb +79 -7
- data/examples/xyzzy.rb +97 -0
- data/lib/robot_lab/agent_skill.rb +5 -4
- data/lib/robot_lab/budget/ledger.rb +98 -0
- data/lib/robot_lab/capabilities.rb +84 -0
- data/lib/robot_lab/config/defaults.yml +10 -0
- data/lib/robot_lab/config.rb +14 -9
- data/lib/robot_lab/error.rb +34 -4
- data/lib/robot_lab/errors.rb +45 -0
- data/lib/robot_lab/hook.rb +79 -0
- data/lib/robot_lab/hook_context.rb +194 -0
- data/lib/robot_lab/hook_registry.rb +55 -0
- data/lib/robot_lab/hooks.rb +87 -0
- data/lib/robot_lab/mcp/connection_poller.rb +2 -2
- data/lib/robot_lab/names.rb +402 -0
- data/lib/robot_lab/narrator.rb +87 -0
- data/lib/robot_lab/network.rb +77 -20
- data/lib/robot_lab/robot/agent_skill_matching.rb +1 -3
- data/lib/robot_lab/robot/budget.rb +89 -0
- data/lib/robot_lab/robot/bus_messaging.rb +90 -18
- data/lib/robot_lab/robot/hooking.rb +56 -0
- data/lib/robot_lab/robot/mcp_management.rb +8 -2
- data/lib/robot_lab/robot/template_rendering.rb +18 -5
- data/lib/robot_lab/robot.rb +233 -89
- data/lib/robot_lab/run_config.rb +24 -5
- data/lib/robot_lab/runnable.rb +51 -0
- data/lib/robot_lab/sandbox/null.rb +13 -0
- data/lib/robot_lab/sandbox/seatbelt.rb +104 -0
- data/lib/robot_lab/sandbox.rb +52 -0
- data/lib/robot_lab/script_tool.rb +60 -2
- data/lib/robot_lab/task.rb +26 -20
- data/lib/robot_lab/tool.rb +52 -11
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +70 -15
- data/mkdocs.yml +7 -1
- metadata +41 -99
- data/examples/temp.md +0 -51
- data/site/404.html +0 -2300
- data/site/api/core/index.html +0 -2706
- data/site/api/core/memory/index.html +0 -3793
- data/site/api/core/network/index.html +0 -3500
- data/site/api/core/robot/index.html +0 -4566
- data/site/api/core/state/index.html +0 -3390
- data/site/api/core/tool/index.html +0 -3843
- data/site/api/index.html +0 -2635
- data/site/api/mcp/client/index.html +0 -3435
- data/site/api/mcp/index.html +0 -2783
- data/site/api/mcp/server/index.html +0 -3252
- data/site/api/mcp/transports/index.html +0 -3352
- data/site/api/messages/index.html +0 -2641
- data/site/api/messages/text-message/index.html +0 -3087
- data/site/api/messages/tool-call-message/index.html +0 -3159
- data/site/api/messages/tool-result-message/index.html +0 -3252
- data/site/api/messages/user-message/index.html +0 -3212
- data/site/api/streaming/context/index.html +0 -3282
- data/site/api/streaming/events/index.html +0 -3347
- data/site/api/streaming/index.html +0 -2738
- data/site/architecture/core-concepts/index.html +0 -3757
- data/site/architecture/index.html +0 -2797
- data/site/architecture/message-flow/index.html +0 -3238
- data/site/architecture/network-orchestration/index.html +0 -3433
- data/site/architecture/robot-execution/index.html +0 -3140
- data/site/architecture/state-management/index.html +0 -3498
- data/site/assets/css/custom.css +0 -56
- data/site/assets/images/favicon.png +0 -0
- data/site/assets/images/robot_lab.jpg +0 -0
- data/site/assets/javascripts/bundle.79ae519e.min.js +0 -16
- data/site/assets/javascripts/bundle.79ae519e.min.js.map +0 -7
- data/site/assets/javascripts/lunr/min/lunr.ar.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.da.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.de.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.du.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.el.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.es.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.fi.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.fr.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.he.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.hi.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.hu.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.hy.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.it.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ja.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.jp.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.kn.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.ko.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.multi.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.nl.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.no.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.pt.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ro.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ru.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.sa.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.sv.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ta.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.te.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.th.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.tr.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.vi.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.zh.min.js +0 -1
- data/site/assets/javascripts/lunr/tinyseg.js +0 -206
- data/site/assets/javascripts/lunr/wordcut.js +0 -6708
- data/site/assets/javascripts/workers/search.2c215733.min.js +0 -42
- data/site/assets/javascripts/workers/search.2c215733.min.js.map +0 -7
- data/site/assets/stylesheets/main.484c7ddc.min.css +0 -1
- data/site/assets/stylesheets/main.484c7ddc.min.css.map +0 -1
- data/site/assets/stylesheets/palette.ab4e12ef.min.css +0 -1
- data/site/assets/stylesheets/palette.ab4e12ef.min.css.map +0 -1
- data/site/concepts/index.html +0 -3455
- data/site/examples/basic-chat/index.html +0 -2880
- data/site/examples/index.html +0 -2907
- data/site/examples/mcp-server/index.html +0 -3018
- data/site/examples/multi-robot-network/index.html +0 -3131
- data/site/examples/rails-application/index.html +0 -3329
- data/site/examples/tool-usage/index.html +0 -3085
- data/site/getting-started/configuration/index.html +0 -3745
- data/site/getting-started/index.html +0 -2572
- data/site/getting-started/installation/index.html +0 -2981
- data/site/getting-started/quick-start/index.html +0 -2942
- data/site/guides/building-robots/index.html +0 -4290
- data/site/guides/creating-networks/index.html +0 -3858
- data/site/guides/index.html +0 -2586
- data/site/guides/mcp-integration/index.html +0 -3581
- data/site/guides/memory/index.html +0 -3586
- data/site/guides/rails-integration/index.html +0 -4019
- data/site/guides/streaming/index.html +0 -3157
- data/site/guides/using-tools/index.html +0 -3802
- data/site/index.html +0 -2671
- data/site/search/search_index.json +0 -1
- data/site/sitemap.xml +0 -183
- data/site/sitemap.xml.gz +0 -0
- data/site/tags.json +0 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
class Robot < RubyLLM::Agent
|
|
5
|
+
# Token/cost budget enforcement for a single Robot, backed by a
|
|
6
|
+
# RobotLab::Budget::Ledger.
|
|
7
|
+
#
|
|
8
|
+
# Two layers of enforcement:
|
|
9
|
+
# - reserve_budget!/reconcile_budget! wrap each LLM call. reserve_budget!
|
|
10
|
+
# raises BudgetExceeded up front when a *prior* call already pushed
|
|
11
|
+
# usage over budget, so a robot with an exhausted budget refuses
|
|
12
|
+
# further calls instead of spending on them.
|
|
13
|
+
# - enforce_token_budget!/enforce_cost_budget! run after the call
|
|
14
|
+
# completes and raise InferenceError when *this* call's actual usage
|
|
15
|
+
# pushed cumulative usage over budget — unavoidable for the call that
|
|
16
|
+
# causes the overage, since token/cost totals aren't known until the
|
|
17
|
+
# response comes back.
|
|
18
|
+
module Budget
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# @return [RobotLab::Budget::Ledger, nil] nil when neither token_budget
|
|
22
|
+
# nor cost_budget is configured
|
|
23
|
+
def build_budget_ledger
|
|
24
|
+
limits = { tokens: @config.token_budget, cost: @config.cost_budget }.compact
|
|
25
|
+
return nil if limits.empty?
|
|
26
|
+
|
|
27
|
+
RobotLab::Budget::Ledger.new(limits: limits)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def enforce_token_budget!
|
|
31
|
+
budget = @config.token_budget
|
|
32
|
+
return unless budget && @total_input_tokens + @total_output_tokens > budget
|
|
33
|
+
|
|
34
|
+
raise InferenceError,
|
|
35
|
+
"Token budget exceeded: #{@total_input_tokens + @total_output_tokens} tokens used, budget is #{budget}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def enforce_cost_budget!
|
|
39
|
+
budget = @config.cost_budget
|
|
40
|
+
return unless budget && @budget_ledger
|
|
41
|
+
|
|
42
|
+
consumed = @budget_ledger.consumed[:cost]
|
|
43
|
+
return unless consumed > budget
|
|
44
|
+
|
|
45
|
+
raise InferenceError,
|
|
46
|
+
format("Cost budget exceeded: $%<consumed>.6f used, budget is $%<budget>.6f",
|
|
47
|
+
consumed: consumed, budget: budget)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Reserves whatever remains of each configured budget dimension
|
|
51
|
+
# before attempting the LLM call.
|
|
52
|
+
#
|
|
53
|
+
# @return [Hash{Symbol=>Numeric}] the amount reserved per dimension, to pass to reconcile_budget!
|
|
54
|
+
# @raise [RobotLab::BudgetExceeded] if a dimension is already exhausted
|
|
55
|
+
def reserve_budget!
|
|
56
|
+
return {} unless @budget_ledger
|
|
57
|
+
|
|
58
|
+
@budget_ledger.limits.each_key.with_object({}) do |dimension, reservation|
|
|
59
|
+
amount = @budget_ledger.remaining(dimension)
|
|
60
|
+
@budget_ledger.reserve!(dimension, amount)
|
|
61
|
+
reservation[dimension] = amount
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Replaces a reservation from reserve_budget! with the actual usage
|
|
66
|
+
# from this call.
|
|
67
|
+
#
|
|
68
|
+
# @param reservation [Hash{Symbol=>Numeric}] the value returned by reserve_budget!
|
|
69
|
+
# @param response [Object] the raw LLM response, used to extract actual cost
|
|
70
|
+
# @param result [RobotResult] the built result, used to extract actual tokens
|
|
71
|
+
def reconcile_budget!(reservation, response:, result:)
|
|
72
|
+
return unless reservation&.any?
|
|
73
|
+
|
|
74
|
+
actual = { tokens: result.input_tokens + result.output_tokens, cost: extract_cost(response) }
|
|
75
|
+
reservation.each_pair { |dimension, amount| @budget_ledger.reconcile!(dimension, amount, actual[dimension] || 0) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @param response [Object] the raw LLM response
|
|
79
|
+
# @return [Float] the response's total cost, or 0.0 when unavailable (no pricing data)
|
|
80
|
+
def extract_cost(response)
|
|
81
|
+
return 0.0 unless response.respond_to?(:cost)
|
|
82
|
+
|
|
83
|
+
response.cost.total.to_f
|
|
84
|
+
rescue StandardError
|
|
85
|
+
0.0
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -12,9 +12,15 @@ module RobotLab
|
|
|
12
12
|
# == Delivery Serialization
|
|
13
13
|
#
|
|
14
14
|
# TypedBus delivers messages in concurrent Async fibers. Robots
|
|
15
|
-
# enqueue deliveries into a BusPoller rather than
|
|
16
|
-
#
|
|
17
|
-
#
|
|
15
|
+
# enqueue deliveries into a shared BusPoller rather than dispatching
|
|
16
|
+
# them straight to the handler. Despite the name, BusPoller runs no
|
|
17
|
+
# background thread: `enqueue` processes the delivery inline in the
|
|
18
|
+
# caller's own execution context (Async fiber or OS thread) when the
|
|
19
|
+
# robot is idle, and otherwise queues it behind the delivery already
|
|
20
|
+
# in flight and drains it once that one returns. A mutex therefore
|
|
21
|
+
# serializes deliveries per robot, so a robot's run() calls never
|
|
22
|
+
# interleave — but nothing makes progress while the calling fiber is
|
|
23
|
+
# parked.
|
|
18
24
|
#
|
|
19
25
|
# Owns: @bus, @bus_poller, @private_bus_poller, @bus_poller_group, @bus_subscriber_id, @message_counter, @outbox, @message_handler
|
|
20
26
|
# Reads: @name
|
|
@@ -29,9 +35,15 @@ module RobotLab
|
|
|
29
35
|
def send_message(to:, content:)
|
|
30
36
|
raise BusError, "No bus configured on robot '#{@name}'" unless @bus
|
|
31
37
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
# Counter + outbox are shared with the poller thread (reply correlation)
|
|
39
|
+
# and with other senders; mutate them under the bus mutex. Publish (which
|
|
40
|
+
# does I/O) stays outside the lock.
|
|
41
|
+
message = @bus_mutex.synchronize do
|
|
42
|
+
@message_counter += 1
|
|
43
|
+
msg = RobotMessage.build(id: @message_counter, from: @name, content: content)
|
|
44
|
+
@outbox[msg.key] = { message: msg, status: :sent, replies: [] }
|
|
45
|
+
msg
|
|
46
|
+
end
|
|
35
47
|
publish_to_bus(to.to_sym, message)
|
|
36
48
|
message
|
|
37
49
|
end
|
|
@@ -46,8 +58,10 @@ module RobotLab
|
|
|
46
58
|
def send_reply(to:, content:, in_reply_to:)
|
|
47
59
|
raise BusError, "No bus configured on robot '#{@name}'" unless @bus
|
|
48
60
|
|
|
49
|
-
@
|
|
50
|
-
|
|
61
|
+
reply = @bus_mutex.synchronize do
|
|
62
|
+
@message_counter += 1
|
|
63
|
+
RobotMessage.build(id: @message_counter, from: @name, content: content, in_reply_to: in_reply_to)
|
|
64
|
+
end
|
|
51
65
|
publish_to_bus(to.to_sym, reply)
|
|
52
66
|
reply
|
|
53
67
|
end
|
|
@@ -65,6 +79,38 @@ module RobotLab
|
|
|
65
79
|
self
|
|
66
80
|
end
|
|
67
81
|
|
|
82
|
+
# Automatically respond to inbound (non-reply) bus tasks: run +responder+ to
|
|
83
|
+
# produce a reply, and send it back to the sender. This is the symmetric
|
|
84
|
+
# counterpart to how a Cyborg answers its human — one call makes any bus
|
|
85
|
+
# member a first-class responder instead of hand-wiring {#on_message}.
|
|
86
|
+
#
|
|
87
|
+
# The responder runs inline in the caller's execution context (BusPoller has
|
|
88
|
+
# no drain thread), and deliveries to this member are serialized, so a long
|
|
89
|
+
# turn blocks both the sender and the next inbound message.
|
|
90
|
+
#
|
|
91
|
+
# @param auto_reply [Boolean] send the responder's result back to the sender
|
|
92
|
+
# @yield [message] the inbound task; return the reply content (nil => no reply)
|
|
93
|
+
# @return [self]
|
|
94
|
+
def respond_to_tasks(auto_reply: true, &responder)
|
|
95
|
+
on_message do |message|
|
|
96
|
+
next if message.reply?
|
|
97
|
+
|
|
98
|
+
reply = responder.call(message)
|
|
99
|
+
send_reply(to: message.from, content: reply, in_reply_to: message.key) if auto_reply && reply
|
|
100
|
+
end
|
|
101
|
+
self
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Serve inbound bus tasks by running each through this member's #run and
|
|
105
|
+
# replying with the result — the one-call way to make a Robot cooperate on
|
|
106
|
+
# the bus the way a Cyborg already does out of the box.
|
|
107
|
+
#
|
|
108
|
+
# @param auto_reply [Boolean]
|
|
109
|
+
# @return [self]
|
|
110
|
+
def serve(auto_reply: true)
|
|
111
|
+
respond_to_tasks(auto_reply: auto_reply) { |message| run(bus_task_content(message)).reply }
|
|
112
|
+
end
|
|
113
|
+
|
|
68
114
|
# Spawn a new robot on a shared bus.
|
|
69
115
|
#
|
|
70
116
|
# Creates a new Robot instance that shares this robot's bus,
|
|
@@ -88,10 +134,24 @@ module RobotLab
|
|
|
88
134
|
template: template,
|
|
89
135
|
local_tools: local_tools,
|
|
90
136
|
bus: @bus,
|
|
137
|
+
**inherited_llm_settings,
|
|
91
138
|
**
|
|
92
139
|
)
|
|
93
140
|
end
|
|
94
141
|
|
|
142
|
+
# Model/provider a spawned robot inherits from its parent so a specialist
|
|
143
|
+
# runs on the SAME LLM as the robot that spawned it (e.g. a local Ollama
|
|
144
|
+
# model) instead of falling back to the global default. Caller-supplied
|
|
145
|
+
# opts override these.
|
|
146
|
+
#
|
|
147
|
+
# @return [Hash]
|
|
148
|
+
def inherited_llm_settings
|
|
149
|
+
settings = {}
|
|
150
|
+
settings[:model] = model if model
|
|
151
|
+
settings[:provider] = provider if provider
|
|
152
|
+
settings
|
|
153
|
+
end
|
|
154
|
+
|
|
95
155
|
# Connect this robot to a message bus.
|
|
96
156
|
#
|
|
97
157
|
# If a bus is provided, the robot joins it. If no bus is provided
|
|
@@ -112,8 +172,9 @@ module RobotLab
|
|
|
112
172
|
|
|
113
173
|
# Assign a shared BusPoller from a Network.
|
|
114
174
|
#
|
|
115
|
-
#
|
|
116
|
-
# the network's shared poller for the given group.
|
|
175
|
+
# Drops any private poller this robot auto-created, then adopts
|
|
176
|
+
# the network's shared poller for the given group. Called by
|
|
177
|
+
# Network#task; groups are informational labels only.
|
|
117
178
|
#
|
|
118
179
|
# @param poller [BusPoller] the network's shared poller
|
|
119
180
|
# @param group [Symbol] poller group for this robot (default: :default)
|
|
@@ -159,21 +220,21 @@ module RobotLab
|
|
|
159
220
|
@bus_poller_group = :default
|
|
160
221
|
end
|
|
161
222
|
|
|
223
|
+
# Flatten a task message's content to text for #run.
|
|
224
|
+
def bus_task_content(message)
|
|
225
|
+
content = message.content
|
|
226
|
+
content.is_a?(Hash) ? content.map { |k, v| "#{k}: #{v}" }.join("\n") : content.to_s
|
|
227
|
+
end
|
|
228
|
+
|
|
162
229
|
# Enqueue a delivery to the robot's assigned poller.
|
|
163
230
|
def enqueue_delivery(delivery)
|
|
164
231
|
@bus_poller.enqueue(robot: self, delivery: delivery, group: @bus_poller_group)
|
|
165
232
|
end
|
|
166
233
|
|
|
167
|
-
# Process a single delivery (called by BusPoller drain
|
|
234
|
+
# Process a single delivery (called by BusPoller during its inline drain).
|
|
168
235
|
def process_delivery(delivery)
|
|
169
236
|
message = delivery.message
|
|
170
|
-
|
|
171
|
-
# Correlate replies with outbox entries
|
|
172
|
-
if message.reply? && @outbox.key?(message.in_reply_to)
|
|
173
|
-
entry = @outbox[message.in_reply_to]
|
|
174
|
-
entry[:status] = :replied
|
|
175
|
-
entry[:replies] << message
|
|
176
|
-
end
|
|
237
|
+
correlate_reply(message) if message.reply?
|
|
177
238
|
|
|
178
239
|
if @message_handler.arity == 1
|
|
179
240
|
delivery.ack!
|
|
@@ -186,6 +247,17 @@ module RobotLab
|
|
|
186
247
|
raise BusError, "Error handling bus message on robot '#{@name}': #{e.message}"
|
|
187
248
|
end
|
|
188
249
|
|
|
250
|
+
# Mark the sender's outbox entry replied. Shared with senders on other
|
|
251
|
+
# threads, so guard with the bus mutex.
|
|
252
|
+
def correlate_reply(message)
|
|
253
|
+
@bus_mutex.synchronize do
|
|
254
|
+
entry = @outbox[message.in_reply_to] or return
|
|
255
|
+
|
|
256
|
+
entry[:status] = :replied
|
|
257
|
+
entry[:replies] << message
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
189
261
|
# Publish a RobotMessage to a bus channel
|
|
190
262
|
def publish_to_bus(channel_name, message)
|
|
191
263
|
if defined?(Async::Task) && Async::Task.current?
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
class Robot < RubyLLM::Agent
|
|
5
|
+
module Hooking
|
|
6
|
+
def run(message = nil, network: nil, task: nil, network_memory: nil, network_config: nil,
|
|
7
|
+
memory: nil, mcp: :none, tools: :none, hooks: nil, **kwargs, &block)
|
|
8
|
+
run_memory = resolve_run_memory(memory, network: network, network_memory: network_memory)
|
|
9
|
+
previous_writer = run_memory.current_writer
|
|
10
|
+
run_memory.current_writer = @name
|
|
11
|
+
context = RunHookContext.new(
|
|
12
|
+
robot: self,
|
|
13
|
+
network: network,
|
|
14
|
+
task: task,
|
|
15
|
+
memory: run_memory,
|
|
16
|
+
config: @config,
|
|
17
|
+
request: message
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
registries = hook_registries(network)
|
|
21
|
+
|
|
22
|
+
begin
|
|
23
|
+
RobotLab.with_hook_scope(registries, hooks) do
|
|
24
|
+
RobotLab::Hooks.run(:run, context, registries: registries, per_run_hooks: hooks) do
|
|
25
|
+
run_context = kwargs.except(:with)
|
|
26
|
+
prepare_tools(message: context.request, mcp: mcp, tools: tools,
|
|
27
|
+
network: network, network_config: network_config)
|
|
28
|
+
rerender_template(run_context) if @template && run_context.any?
|
|
29
|
+
reservation = reserve_budget!
|
|
30
|
+
response = invoke_ask(context: context, kwargs: kwargs, hooks: hooks, block: block)
|
|
31
|
+
result = build_result(response, run_memory)
|
|
32
|
+
reconcile_budget!(reservation, response: response, result: result)
|
|
33
|
+
enforce_token_budget!
|
|
34
|
+
enforce_cost_budget!
|
|
35
|
+
result
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
ensure
|
|
39
|
+
remove_doom_loop_detection
|
|
40
|
+
restore_tool_call_callback if @config.max_tool_rounds
|
|
41
|
+
run_memory.current_writer = previous_writer
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def on(handler_class, context: nil)
|
|
46
|
+
@hooks.on(handler_class, context: context)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def hook_registries(network = nil)
|
|
52
|
+
[RobotLab.hooks, network&.hooks, @hooks]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -12,18 +12,24 @@ module RobotLab
|
|
|
12
12
|
|
|
13
13
|
# Resolve MCP hierarchy: runtime -> robot build -> network -> config
|
|
14
14
|
def resolve_mcp_hierarchy(runtime_value, network: nil, network_config: nil)
|
|
15
|
-
parent_value = network_config&.mcp || network&.
|
|
15
|
+
parent_value = network_config&.mcp || network_parent_config(network)&.mcp || RobotLab.config.mcp
|
|
16
16
|
build_resolved = ToolConfig.resolve_mcp(@mcp_config, parent_value: parent_value)
|
|
17
17
|
ToolConfig.resolve_mcp(runtime_value, parent_value: build_resolved)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
# Resolve tools hierarchy: runtime -> robot build -> network -> config
|
|
21
21
|
def resolve_tools_hierarchy(runtime_value, network: nil, network_config: nil)
|
|
22
|
-
parent_value = network_config&.tools || network&.
|
|
22
|
+
parent_value = network_config&.tools || network_parent_config(network)&.tools || RobotLab.config.tools
|
|
23
23
|
build_resolved = ToolConfig.resolve_tools(@tools_config, parent_value: parent_value)
|
|
24
24
|
ToolConfig.resolve_tools(runtime_value, parent_value: build_resolved)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
def network_parent_config(network)
|
|
28
|
+
return network.config if network.respond_to?(:config)
|
|
29
|
+
|
|
30
|
+
network&.network
|
|
31
|
+
end
|
|
32
|
+
|
|
27
33
|
# Ensure MCP clients are initialized for the given server configs.
|
|
28
34
|
# On subsequent calls, retries any servers that previously failed to connect.
|
|
29
35
|
def ensure_mcp_clients(mcp_servers)
|
|
@@ -45,7 +45,7 @@ module RobotLab
|
|
|
45
45
|
# Front matter is the base; @config (from constructor kwargs) overrides.
|
|
46
46
|
fm_config = RunConfig.from_front_matter(parsed.metadata)
|
|
47
47
|
effective = fm_config.merge(@config)
|
|
48
|
-
effective.apply_to(@chat)
|
|
48
|
+
effective.apply_to(@chat, provider: @provider, assume_model_exists: !@provider.nil?)
|
|
49
49
|
|
|
50
50
|
# Resolve context (could be a Proc)
|
|
51
51
|
resolved_ctx = resolve_context(context, network: nil)
|
|
@@ -64,6 +64,10 @@ module RobotLab
|
|
|
64
64
|
|
|
65
65
|
# Re-render the template with run-time context merged into build-time context.
|
|
66
66
|
# prompt_manager parameters may be required (null) and only available at run time.
|
|
67
|
+
#
|
|
68
|
+
# Re-rendering replaces the system message, so the inline system_prompt must be
|
|
69
|
+
# re-appended here exactly as apply_system_prompt does at construction --
|
|
70
|
+
# otherwise it would be silently dropped on any run that supplies context.
|
|
67
71
|
def rerender_template(run_context)
|
|
68
72
|
merged = (@build_context || {}).merge(run_context)
|
|
69
73
|
resolved_ctx = resolve_context(merged, network: nil)
|
|
@@ -81,15 +85,24 @@ module RobotLab
|
|
|
81
85
|
bodies << body if body
|
|
82
86
|
end
|
|
83
87
|
|
|
84
|
-
|
|
85
|
-
@chat.with_instructions(combined)
|
|
88
|
+
@chat.with_instructions(with_system_prompt_appended(bodies.join("\n\n")))
|
|
86
89
|
else
|
|
87
90
|
parsed = PM.parse(@template)
|
|
88
91
|
rendered = parsed.to_s(**resolved_ctx)
|
|
89
|
-
@chat.with_instructions(rendered)
|
|
92
|
+
@chat.with_instructions(with_system_prompt_appended(rendered))
|
|
90
93
|
end
|
|
91
94
|
end
|
|
92
95
|
|
|
96
|
+
# Append the inline system_prompt to freshly rendered template text.
|
|
97
|
+
#
|
|
98
|
+
# @param rendered [String] newly rendered template body
|
|
99
|
+
# @return [String] rendered text with system_prompt appended, if one is set
|
|
100
|
+
def with_system_prompt_appended(rendered)
|
|
101
|
+
return rendered unless @system_prompt
|
|
102
|
+
|
|
103
|
+
[rendered, @system_prompt].reject { |s| s.to_s.empty? }.join("\n\n")
|
|
104
|
+
end
|
|
105
|
+
|
|
93
106
|
# Orchestrate skill expansion and template application.
|
|
94
107
|
#
|
|
95
108
|
# @param skill_ids [Array<Symbol>] skill IDs from constructor + front matter
|
|
@@ -142,7 +155,7 @@ module RobotLab
|
|
|
142
155
|
apply_accumulated_extras(extras)
|
|
143
156
|
|
|
144
157
|
effective = accumulated_config.merge(@config)
|
|
145
|
-
effective.apply_to(@chat)
|
|
158
|
+
effective.apply_to(@chat, provider: @provider, assume_model_exists: !@provider.nil?)
|
|
146
159
|
|
|
147
160
|
combined = bodies.join("\n\n")
|
|
148
161
|
@chat.with_instructions(combined) unless combined.empty?
|