Module: Legion::LLM::Inference::Executor::ToolInjection

Included in:
Legion::LLM::Inference::Executor
Defined in:
lib/legion/llm/inference/executor/tool_injection.rb

Overview

ToolInjection methods extracted from Executor verbatim (P4b §1.5, refactor-under-green). Builds the per-request native tool catalog (special-pinned + caller-supplied + registry- discovered + GAIA-triggered) with passthrough policy gating, dedup variants, and per-tier injection limits. Memoized via @native_dispatch_tools / @native_tool_definitions — those memos are invalidated implicitly by Executor lifecycle (one Executor per request).

Constant Summary collapse

GENERATION_PARAMS =

Propagate generation sampling params from the canonical request into the dispatch options hash. Uses .key? so that explicit 0 values are preserved (0 is a valid temperature, not the same as absent/nil).

%i[temperature top_p top_k frequency_penalty presence_penalty seed].freeze

Instance Method Summary collapse

Instance Method Details

#add_native_tool_definition(definitions, tool) ⇒ Object



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 166

def add_native_tool_definition(definitions, tool)
  source = request_tool_source(tool)
  definition = case tool
               when Types::ToolDefinition
                 source == tool.source ? tool : tool.with(source: source)
               when Hash
                 Types::ToolDefinition.from_hash(tool, source: source)
               else
                 Types::ToolDefinition.from_tool_class(tool)
               end
  if non_executable_client_tool?(definition) && !client_tool_passthrough_enabled?
    log.info(
      "[llm][tools][inject] action=client_tool_skipped request_id=#{request_log_value(:id, 'unknown')} " \
      "conversation_id=#{request_log_value(:conversation_id, 'none') || 'none'} name=#{definition.name} " \
      'reason=client_passthrough_not_enabled'
    )
    return
  end
  if non_executable_client_tool?(definition) && !client_tool_passthrough_allowed?(definition)
    log.info(
      "[llm][tools][inject] action=client_tool_skipped request_id=#{request_log_value(:id, 'unknown')} " \
      "conversation_id=#{request_log_value(:conversation_id, 'none') || 'none'} name=#{definition.name} " \
      'reason=client_passthrough_policy'
    )
    return
  end
  return if gaia_tool_suppressed?(definition.name)
  return if native_tool_definition_duplicate?(definitions, definition)

  @injected_tool_map[definition.name] = definition.source[:tool_class] if definition.source[:tool_class]
  @native_tool_source_map[definition.name] = definition.source
  definitions << definition
rescue StandardError => e
  @warnings << "Failed to define tool: #{e.message}"
  handle_exception(e, level: :warn, operation: 'llm.pipeline.native_tool_definition')
end

#add_pinned_special_tool_definitions(definitions) ⇒ Object



157
158
159
160
161
162
163
164
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 157

def add_pinned_special_tool_definitions(definitions)
  Tools::Special.pinned_definitions.each do |definition|
    next if definitions.any? { |existing| existing.name == definition.name }

    @native_tool_source_map[definition.name] = definition.source
    definitions << definition
  end
end

#add_registry_tool_definitions(definitions) ⇒ Object



286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 286

def add_registry_tool_definitions(definitions)
  return unless registry_tool_sources_available?

  count_before = definitions.size
  add_settings_extensions_tool_definitions(definitions)
  count_after = definitions.size
  log.info(
    "[llm][tools] registry_injection count_before=#{count_before} count_after=#{count_after} " \
    "added=#{count_after - count_before} limit=#{registry_tool_limit}"
  )
rescue StandardError => e
  @warnings << "Tool definition error: #{e.message}"
  handle_exception(e, level: :error, operation: 'llm.pipeline.native_registry_tools')
end

#add_requested_deferred_tool_definitions_from_settings(definitions, injected_names) ⇒ Object



353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 353

def add_requested_deferred_tool_definitions_from_settings(definitions, injected_names)
  requested = requested_deferred_tool_names
  return if requested.empty?

  deferred_entries = Legion::Settings::Extensions.filter_tools(deferred: true)
  deferred_entries.each do |entry|
    definition = Types::ToolDefinition.from_registry_entry(entry)
    next unless requested.include?(definition.name)
    next if gaia_tool_suppressed?(definition.name)
    next if injected_names.include?(definition.name)

    @injected_tool_map[definition.name] = entry[:tool_class] if entry[:tool_class]
    @native_tool_source_map[definition.name] = definition.source
    definitions << definition
    injected_names << definition.name
  end
end

#add_settings_extensions_tool_definitions(definitions) ⇒ Object



317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 317

def add_settings_extensions_tool_definitions(definitions)
  existing_names = definitions.map(&:name)
  inject_limit = registry_tool_limit
  registry_added = 0

  always_entries = Legion::Settings::Extensions.filter_tools(deferred: false)
  gaia_entries = gaia_advisory_tool_entries
  triggered_entries = @triggered_tools.any? ? Array(@triggered_tools) : []
  prioritized = if local_provider?
                  gaia_entries + triggered_entries + always_entries
                else
                  always_entries + gaia_entries + triggered_entries
                end

  prioritized.each do |entry|
    break if inject_limit && registry_added >= inject_limit

    definition = if entry.is_a?(Hash) && entry[:name]
                   Types::ToolDefinition.from_registry_entry(entry)
                 else
                   Types::ToolDefinition.from_tool_class(entry)
                 end
    next if gaia_tool_suppressed?(definition.name)
    next if existing_names.include?(definition.name)

    tool_class = entry.is_a?(Hash) ? entry[:tool_class] : entry
    @injected_tool_map[definition.name] = tool_class if tool_class
    @native_tool_source_map[definition.name] = definition.source
    definitions << definition
    existing_names << definition.name
    registry_added += 1
  end

  add_requested_deferred_tool_definitions_from_settings(definitions, existing_names)
end

#apply_generation_params!(options) ⇒ Object



59
60
61
62
63
64
65
66
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 59

def apply_generation_params!(options)
  generation = @request.generation
  return unless generation.is_a?(Hash)

  GENERATION_PARAMS.each do |param|
    options[param] = generation[param] if generation.key?(param)
  end
end

#client_tool_passthrough_allowed?(definition) ⇒ Boolean

Returns:

  • (Boolean)


117
118
119
120
121
122
123
124
125
126
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 117

def client_tool_passthrough_allowed?(definition)
  names = client_tool_passthrough_name_variants(definition)
  whitelist = client_tool_passthrough_list(:client_tool_passthrough_whitelist)
  blacklist = client_tool_passthrough_list(:client_tool_passthrough_blacklist)

  return false if whitelist.any? && !names.intersect?(whitelist)
  return false if names.intersect?(blacklist)

  true
end

#client_tool_passthrough_enabled?Boolean

Returns:

  • (Boolean)


107
108
109
110
111
112
113
114
115
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 107

def client_tool_passthrough_enabled?
  if @request.respond_to?(:metadata)
     = @request. || {}
    value = .key?(:client_tool_passthrough) ? [:client_tool_passthrough] : ['client_tool_passthrough']
    return value if [true, false].include?(value)
  end

  Legion::Settings.dig(:llm, :tools, :trigger, :client_tool_passthrough) == true
end

#client_tool_passthrough_list(key) ⇒ Object



128
129
130
131
132
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 128

def client_tool_passthrough_list(key)
  Array(Legion::Settings.dig(:llm, :tools, :trigger, key)).flat_map do |entry|
    client_tool_policy_variants(entry)
  end.uniq
end

#client_tool_passthrough_name_variants(definition) ⇒ Object



134
135
136
137
138
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 134

def client_tool_passthrough_name_variants(definition)
  source = definition.respond_to?(:source) ? definition.source : {}
  raw_name = source[:raw_name] || source['raw_name'] if source.is_a?(Hash)
  [definition.name, raw_name].compact.flat_map { |name| client_tool_policy_variants(name) }.uniq
end

#client_tool_policy_variants(value) ⇒ Object



140
141
142
143
144
145
146
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 140

def client_tool_policy_variants(value)
  raw = value.to_s.strip.downcase
  sanitized = Types::ToolDefinition.sanitize_tool_name(value).downcase
  compact = raw.gsub(/[^a-z0-9]/, '')

  [raw, sanitized, compact].reject(&:empty?).uniq
end

#native_dispatch_chat_optionsObject



68
69
70
71
72
73
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 68

def native_dispatch_chat_options
  opts = { model: @resolved_model, provider: @resolved_provider }
  opts[:instance] = @resolved_instance if @resolved_instance
  opts[:thinking] = native_dispatch_thinking if native_dispatch_thinking
  opts.compact
end

#native_dispatch_optionsObject



13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 13

def native_dispatch_options
  injected_system = if @native_tool_loop_round.to_i.positive?
                      @cached_injected_system
                    else
                      @cached_injected_system = EnrichmentInjector.inject(
                        system:      @request.system,
                        enrichments: @enrichments
                      )
                    end

  record_system_accounting(injected_system) if @native_tool_loop_round.to_i.zero?

  options = {
    system:            injected_system,
    offering_id:       @resolved_offering_id,
    offering_metadata: @resolved_offering_metadata
  }
  options[:system] = native_tool_loop_system(options[:system])
  options[:tools] = native_dispatch_tools if native_dispatch_tools.any?
  options[:tool_prefs] = native_tool_prefs if native_dispatch_tools.any? && native_tool_prefs
  options[:thinking] = native_dispatch_thinking if native_dispatch_thinking
  apply_generation_params!(options)
  options.compact
end

#native_dispatch_thinkingObject



75
76
77
78
79
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 75

def native_dispatch_thinking
  return @request.thinking if @request.thinking

  nil
end

#native_dispatch_toolsObject



81
82
83
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 81

def native_dispatch_tools
  @native_dispatch_tools ||= native_tool_definitions.to_h { |tool| [tool.name.to_sym, tool.to_h] }
end

#native_tool_definition_duplicate?(definitions, definition) ⇒ Boolean

Returns:

  • (Boolean)


301
302
303
304
305
306
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 301

def native_tool_definition_duplicate?(definitions, definition)
  candidate_names = native_tool_definition_name_variants(definition)
  definitions.any? do |existing|
    native_tool_definition_name_variants(existing).intersect?(candidate_names)
  end
end

#native_tool_definition_name_variants(definition) ⇒ Object



308
309
310
311
312
313
314
315
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 308

def native_tool_definition_name_variants(definition)
  variants = client_tool_passthrough_name_variants(definition)
  source = definition.respond_to?(:source) ? definition.source : {}
  source_type = nil
  source_type = source[:type] || source['type'] if source.is_a?(Hash)
  variants += Tools::Special.aliases_for(definition.name).flat_map { |name| client_tool_policy_variants(name) } if source_type.respond_to?(:to_sym) && source_type.to_sym == :special
  variants.uniq
end

#native_tool_definitionsObject



85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 85

def native_tool_definitions
  @native_tool_definitions ||= begin
    definitions = []
    add_pinned_special_tool_definitions(definitions)
    Array(@request.tools).each { |tool| add_native_tool_definition(definitions, tool) }
    add_registry_tool_definitions(definitions) if registry_tool_injection_requested?
    record_tool_accounting(definitions)
    log.debug "[llm][executor] action=native_tool_definitions.built count=#{definitions.size}"
    log_native_tool_definitions(definitions)
    definitions
  end
end

#native_tool_loop_continuation_promptObject



44
45
46
47
48
49
50
51
52
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 44

def native_tool_loop_continuation_prompt
  <<~PROMPT.strip
    Tool-use continuation rule:
    - You just received tool results.
    - If a tool failed or produced incomplete information and another available tool can continue the user's request, call that tool now.
    - Do not say you will use a tool unless you are actually making the tool call in this response.
    - Only provide a final answer when no further tool call is needed or possible.
  PROMPT
end

#native_tool_loop_system(system) ⇒ Object



38
39
40
41
42
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 38

def native_tool_loop_system(system)
  return system unless @native_tool_loop_round.to_i.positive? && native_dispatch_tools.any?

  [system, native_tool_loop_continuation_prompt].compact.join("\n\n")
end

#non_executable_client_tool?(definition) ⇒ Boolean

Returns:

  • (Boolean)


148
149
150
151
152
153
154
155
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 148

def non_executable_client_tool?(definition)
  source = definition.respond_to?(:source) ? definition.source : {}
  return false unless source.is_a?(Hash)

  source_type = source[:type] || source['type']
  executable = source.key?(:executable) ? source[:executable] : source['executable']
  source_type.respond_to?(:to_sym) && source_type.to_sym == :client && executable != true
end

#record_system_accounting(injected_system) ⇒ Object



390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 390

def record_system_accounting(injected_system)
  baseline = EnrichmentInjector.resolve_baseline
  baseline_tokens = ContextAccounting.estimate_text_tokens(baseline)
  system_tokens = ContextAccounting.estimate_text_tokens(injected_system)
  @context_accounting[:tokens][:baseline_system_estimated_tokens] = baseline_tokens
  @context_accounting[:tokens][:system_prompt_estimated_tokens] = system_tokens
  @context_accounting[:component_status][:system] = :observed
  @context_accounting[:events] << ContextAccounting.event(
    event_type:    :system_injected,
    component:     :system,
    before_tokens: 0,
    after_tokens:  system_tokens,
    metadata:      { baseline_tokens: baseline_tokens }
  )
rescue StandardError => e
  handle_exception(e, level: :warn, operation: 'llm.pipeline.record_system_accounting')
end

#record_tool_accounting(definitions) ⇒ Object



371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 371

def record_tool_accounting(definitions)
  return if definitions.empty?

  tool_tokens = ContextAccounting.estimate_json_tokens(definitions.map(&:to_h))
  @context_accounting[:tokens][:tool_definition_estimated_tokens] = tool_tokens
  @context_accounting[:counts][:tool_definition_count] = definitions.size
  @context_accounting[:component_status][:tools] = :observed
  @context_accounting[:events] << ContextAccounting.event(
    event_type:    :tools_injected,
    component:     :tools,
    before_tokens: 0,
    after_tokens:  tool_tokens,
    before_count:  0,
    after_count:   definitions.size
  )
rescue StandardError => e
  handle_exception(e, level: :warn, operation: 'llm.pipeline.record_tool_accounting')
end

#registry_tool_injection_requested?Boolean

Returns:

  • (Boolean)


98
99
100
101
102
103
104
105
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 98

def registry_tool_injection_requested?
  return false if @request.respond_to?(:suppress_tools) && @request.suppress_tools

  # Always inject LegionIO tools (special + extension). Client passthrough
  # is handled by the tool loop, which executes LegionIO tools server-side
  # and returns only client tools to the client.
  true
end

#request_tool_names(tool) ⇒ Object



264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 264

def request_tool_names(tool)
  explicit_source = if tool.respond_to?(:source)
                      tool.source
                    elsif tool.respond_to?(:[])
                      tool[:source] || tool['source']
                    end
  source = explicit_source.is_a?(Hash) ? explicit_source : {}
  raw_name = source[:raw_name] || source['raw_name']
  declared_name = if tool.respond_to?(:name)
                    tool.name
                  elsif tool.respond_to?(:[])
                    tool[:name] || tool['name']
                  end

  [raw_name, declared_name].compact.flat_map do |name|
    raw = name.to_s
    sanitized = Types::ToolDefinition.sanitize_tool_name(raw)
    dotted_legion = sanitized.sub(/\Alegion_/, 'legion.')
    [raw, sanitized, dotted_legion]
  end.uniq
end

#request_tool_source(tool) ⇒ Object



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 203

def request_tool_source(tool)
  explicit_source = if tool.respond_to?(:source)
                      tool.source
                    elsif tool.respond_to?(:[])
                      tool[:source] || tool['source']
                    end

  source = explicit_source.is_a?(Hash) ? explicit_source : {}
  source_type = source[:type] || source['type']

  client_like_source = source.empty? ||
                       (source_type.respond_to?(:to_sym) && source_type.to_sym == :client)

  # Allow client-shaped declarations to be reclassified to LegionIO sources
  # when they match tools registered on this node.
  if client_like_source
    resolved_source = resolve_registry_tool_source(tool)
    return resolved_source if resolved_source
  end

  source.empty? ? { type: :client, executable: false } : source
end

#resolve_registry_tool_source(tool) ⇒ Object



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 226

def resolve_registry_tool_source(tool)
  tool_name = request_tool_names(tool).find { |name| !name.to_s.empty? }
  return nil unless tool_name
  return nil unless Legion::Settings::Extensions.respond_to?(:find_tool)

  entry = request_tool_names(tool).filter_map do |candidate|
    Legion::Settings::Extensions.find_tool(candidate)
  end.first
  return nil unless entry.is_a?(Hash)

  tool_class = entry[:tool_class] || entry['tool_class']
  extension = entry[:extension] || entry['extension']
  runner = entry[:runner] || entry['runner']
  function = entry[:function] || entry['function']

  if tool_class
    return {
      type:       :registry,
      tool_class: tool_class,
      extension:  extension,
      runner:     runner,
      function:   function
    }.compact
  end

  return nil unless extension.to_s.length.positive? && runner.to_s.length.positive? && function.to_s.length.positive?

  {
    type:     :extension,
    lex:      extension,
    runner:   runner,
    function: function
  }
rescue StandardError => e
  handle_exception(e, level: :warn, operation: 'llm.pipeline.native_tool_source.resolve', tool_name: tool_name)
  nil
end