Class: RubyPi::Tools::Executor

Inherits:
Object
  • Object
show all
Defined in:
lib/ruby_pi/tools/executor.rb

Constant Summary collapse

DEFAULT_TIMEOUT =

Default timeout for each tool execution, in seconds.

30

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(registry, mode: :parallel, timeout: DEFAULT_TIMEOUT) ⇒ Executor

Creates a new Executor.

Parameters:

  • registry (RubyPi::Tools::Registry)

    The registry to look up tools from.

  • mode (Symbol) (defaults to: :parallel)

    Execution mode — :parallel or :sequential.

  • timeout (Numeric) (defaults to: DEFAULT_TIMEOUT)

    Per-tool timeout in seconds (default: 30).

Raises:

  • (ArgumentError)

    If mode is not :parallel or :sequential.



44
45
46
47
48
49
50
51
52
# File 'lib/ruby_pi/tools/executor.rb', line 44

def initialize(registry, mode: :parallel, timeout: DEFAULT_TIMEOUT)
  unless %i[parallel sequential].include?(mode)
    raise ArgumentError, "Mode must be :parallel or :sequential, got #{mode.inspect}"
  end

  @registry = registry
  @mode = mode
  @timeout = timeout
end

Instance Attribute Details

#modeSymbol (readonly)

Returns The execution mode (:parallel or :sequential).

Returns:

  • (Symbol)

    The execution mode (:parallel or :sequential).



33
34
35
# File 'lib/ruby_pi/tools/executor.rb', line 33

def mode
  @mode
end

#timeoutNumeric (readonly)

Returns The per-tool timeout in seconds.

Returns:

  • (Numeric)

    The per-tool timeout in seconds.



36
37
38
# File 'lib/ruby_pi/tools/executor.rb', line 36

def timeout
  @timeout
end

Class Method Details

.deep_symbolize_keys(obj) ⇒ Object

Recursively converts all string keys in a hash to symbols so that tool implementations can use idiomatic Ruby symbol-key access (e.g. ‘args`) regardless of whether the LLM provider returned string-keyed JSON. Exposed as a class method so the agent loop can apply the same transformation to tool_call arguments before recording them in `tool_calls_made`, keeping the agent’s observable arguments shape consistent with what tool blocks see.

Parameters:

  • obj (Object)

    the object to transform (Hash, Array, or scalar)

Returns:

  • (Object)

    the transformed object with symbolized keys



255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/ruby_pi/tools/executor.rb', line 255

def self.deep_symbolize_keys(obj)
  case obj
  when Hash
    obj.each_with_object({}) do |(key, value), result|
      result[key.to_sym] = deep_symbolize_keys(value)
    end
  when Array
    obj.map { |item| deep_symbolize_keys(item) }
  else
    obj
  end
end

Instance Method Details

#execute(calls) ⇒ Array<RubyPi::Tools::Result>

Executes a list of tool calls and returns their results.

Each call is a hash with ‘:name` (String or Symbol) and `:arguments` (Hash). Tools are looked up in the registry; if a tool is not found, a failure Result is returned for that call.

Issue #17: Raises NoToolsRegisteredError if the registry is nil and tool calls are attempted, preventing a confusing NoMethodError.

Parameters:

  • calls (Array<Hash>)

    Tool call requests, each with :name and :arguments.

Returns:

Raises:



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/ruby_pi/tools/executor.rb', line 66

def execute(calls)
  # Issue #17: Guard against nil registry — if the LLM hallucinated tool
  # calls but no tools are registered, raise a typed error immediately
  # rather than crashing with NoMethodError on nil.find.
  if @registry.nil?
    raise RubyPi::NoToolsRegisteredError,
          "Model returned #{calls.size} tool call(s) but no tools are registered"
  end

  case @mode
  when :parallel
    execute_parallel(calls)
  when :sequential
    execute_sequential(calls)
  end
end