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
DEFAULT_MAX_CALLS =
32
DEFAULT_MAX_CONCURRENCY =
4
MAX_ACTIVE_EXECUTIONS =
16

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(registry, mode: :parallel, timeout: DEFAULT_TIMEOUT, max_calls: DEFAULT_MAX_CALLS, max_concurrency: DEFAULT_MAX_CONCURRENCY) ⇒ 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.



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/ruby_pi/tools/executor.rb', line 87

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

  validate_positive_numeric!(:timeout, timeout)
  validate_positive_integer!(:max_calls, max_calls)
  validate_positive_integer!(:max_concurrency, max_concurrency)

  @registry = registry
  @mode = mode
  @timeout = timeout
  @max_calls = max_calls
  @max_concurrency = max_concurrency
end

Instance Attribute Details

#modeSymbol (readonly)

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

Returns:

  • (Symbol)

    The execution mode (:parallel or :sequential).



62
63
64
# File 'lib/ruby_pi/tools/executor.rb', line 62

def mode
  @mode
end

#timeoutNumeric (readonly)

Returns The per-tool timeout in seconds.

Returns:

  • (Numeric)

    The per-tool timeout in seconds.



65
66
67
# File 'lib/ruby_pi/tools/executor.rb', line 65

def timeout
  @timeout
end

Class Method Details

.active_execution_countObject



39
40
41
# File 'lib/ruby_pi/tools/executor.rb', line 39

def active_execution_count
  @capacity_mutex.synchronize { @active_executions }
end

.deep_symbolize_keys(obj) ⇒ Object

Recursively converts all string keys in a hash to symbols.



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

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:



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/ruby_pi/tools/executor.rb', line 116

def execute(calls)
  unless calls.is_a?(Array)
    raise ArgumentError, "calls must be an Array, got #{calls.class}"
  end

  if calls.size > @max_calls
    raise RubyPi::ToolCallLimitError,
          "Model returned #{calls.size} tool calls; maximum is #{@max_calls} per execution"
  end

  return [] if calls.empty?

  if Executor.active_execution_count >= MAX_ACTIVE_EXECUTIONS
    raise RubyPi::ToolExecutionCapacityError,
          "Refusing tool execution while #{MAX_ACTIVE_EXECUTIONS} tools are still running"
  end

  # 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