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).

Instance Method Summary collapse

Instance Method Details

#add_native_tool_definition(definitions, tool) ⇒ Object



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 151

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



142
143
144
145
146
147
148
149
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 142

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



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

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



338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 338

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



302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 302

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

#client_tool_passthrough_allowed?(definition) ⇒ Boolean

Returns:

  • (Boolean)


102
103
104
105
106
107
108
109
110
111
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 102

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)


92
93
94
95
96
97
98
99
100
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 92

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, :tool_trigger, :client_tool_passthrough) == true
end

#client_tool_passthrough_list(key) ⇒ Object



113
114
115
116
117
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 113

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

#client_tool_passthrough_name_variants(definition) ⇒ Object



119
120
121
122
123
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 119

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



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

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



53
54
55
56
57
58
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 53

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
# 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
  options.compact
end

#native_dispatch_thinkingObject



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

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

  nil
end

#native_dispatch_toolsObject



66
67
68
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 66

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)


286
287
288
289
290
291
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 286

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



293
294
295
296
297
298
299
300
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 293

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



70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 70

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



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

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



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

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)


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

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



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

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



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

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)


83
84
85
86
87
88
89
90
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 83

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



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 249

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



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 188

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



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/legion/llm/inference/executor/tool_injection.rb', line 211

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: :debug, operation: 'llm.pipeline.native_tool_source.resolve', tool_name: tool_name)
  nil
end