Class: Rubino::Agent::ToolExecutor
- Inherits:
-
Object
- Object
- Rubino::Agent::ToolExecutor
- Defined in:
- lib/rubino/agent/tool_executor.rb
Overview
Executes tool calls with approval checks and result formatting.
Instance Attribute Summary collapse
-
#on_result ⇒ Object
writeonly
The Loop registers its count+persist sink here after construction (the executor is built first so the adapter/ToolBridge can share it).
Instance Method Summary collapse
-
#execute(name:, arguments:, call_id:) ⇒ Object
Executes a single tool call, returns a Tools::Result.
-
#initialize(registry:, approval_policy:, ui:, config:, tool_call_repository: Tools::ToolCallRepository.new, cancel_token: nil, read_tracker: nil, event_bus: nil, on_result: nil) ⇒ ToolExecutor
constructor
A new instance of ToolExecutor.
Constructor Details
#initialize(registry:, approval_policy:, ui:, config:, tool_call_repository: Tools::ToolCallRepository.new, cancel_token: nil, read_tracker: nil, event_bus: nil, on_result: nil) ⇒ ToolExecutor
Returns a new instance of ToolExecutor.
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/rubino/agent/tool_executor.rb', line 12 def initialize(registry:, approval_policy:, ui:, config:, tool_call_repository: Tools::ToolCallRepository.new, cancel_token: nil, read_tracker: nil, event_bus: nil, on_result: nil) @registry = registry @approval_policy = approval_policy @ui = ui @config = config @tool_call_repository = tool_call_repository @cancel_token = cancel_token # Optional sink the Loop registers so a tool that runs on the STREAMING # path (ruby_llm dispatches it mid-stream via ToolBridge → straight into # #execute, never returning through Loop#execute_tool_calls) is still # counted in the turn summary and persisted as a `tool` message. Called # once per completed/denied tool with (name:, arguments:, call_id:, # result:). The non-streaming path routes through the same sink so the # count/persist happens in exactly one place regardless of mode. @on_result = on_result # Optional event bus so this executor emits TOOL_STARTED/TOOL_FINISHED # for the API mode timeline. ToolBridge already emits these when no # executor is wired (test/one-shot path); the production path went # through here and dropped them, so the web UI timeline never saw # the tool call as a discrete event. @event_bus = event_bus # One tracker shared across every tool call so the read registered by # ReadTool is visible to a later EditTool. The production path # (Interaction::Lifecycle) injects the SESSION-scoped tracker so the # gate spans turns (#151). Default to a fresh tracker if the caller # didn't supply one; an isolated unit test can pass # `read_tracker: nil` to skip the gate. @read_tracker = read_tracker.equal?(false) ? nil : (read_tracker || Tools::ReadTracker.new) end |
Instance Attribute Details
#on_result=(value) ⇒ Object (writeonly)
The Loop registers its count+persist sink here after construction (the executor is built first so the adapter/ToolBridge can share it). See Loop#handle_tool_result.
10 11 12 |
# File 'lib/rubino/agent/tool_executor.rb', line 10 def on_result=(value) @on_result = value end |
Instance Method Details
#execute(name:, arguments:, call_id:) ⇒ Object
Executes a single tool call, returns a Tools::Result.
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/rubino/agent/tool_executor.rb', line 46 def execute(name:, arguments:, call_id:) tool = @registry.find(name) raise ToolError, "Unknown tool: #{name}" unless tool case @approval_policy.decide(tool, arguments: arguments) when :deny # A policy denial must NOT read "denied by user" to the model — the # policy records why it fired (#last_deny_reason) and the Result # maps it to a reason-specific message, so a child agent never # blames the human for an automatic deny (#143). denied = Tools::Result.denied(name: name, call_id: call_id, reason: policy_deny_reason) record_denied(name: name, call_id: call_id, arguments: arguments, result: denied, reason: "policy-denied") return finish(name, arguments, call_id, denied) when :ask unless request_approval(tool, arguments) denied = Tools::Result.denied(name: name, call_id: call_id, reason: :user) record_denied(name: name, call_id: call_id, arguments: arguments, result: denied, reason: "user-denied") return finish(name, arguments, call_id, denied) end end notify_yolo_if_applicable(tool, arguments) emit_started(name, arguments) started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) result = nil begin result = run_tool(tool, name: name, arguments: arguments, call_id: call_id) ensure duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round emit_artifact(result) if result.respond_to?(:artifact) && result&.artifact emit_finished(name, result: result, duration_ms: duration_ms, arguments: arguments) end finish(name, arguments, call_id, result) end |