Class: Insika::ToolAssembly

Inherits:
Object
  • Object
show all
Defined in:
lib/insika/tool_assembly.rb

Overview

Turn-scoped assembly of the agent's tool instances (pipeline stage 3 tail): capability resolution, instantiation (Entry#factory | ready instance), turn-context (D2) injection, the capability<->direct dedup join, and the ToolEnvelope wrap (stage 7 seam: per-call timeout + side-effect recording).

Extracted from the Executor (§11 B5) to keep the hot-path file smaller. It is a pure collaborator — it holds only the injected registries/stores and no per-turn state; everything turn-specific arrives via state/turn_context. The Executor keeps thin delegators (resolve_capabilities/assemble_tool_instances/ wrap_tools) so the existing private-method contract stays intact.

Instance Method Summary collapse

Constructor Details

#initialize(tool_registry:, capability_registry:, event_stream:, checkpoint_store:, tool_trace_store:) ⇒ ToolAssembly

Returns a new instance of ToolAssembly.



18
19
20
21
22
23
24
25
# File 'lib/insika/tool_assembly.rb', line 18

def initialize(tool_registry:, capability_registry:, event_stream:,
               checkpoint_store:, tool_trace_store:)
  @tool_registry = tool_registry
  @capability_registry = capability_registry
  @event_stream = event_stream
  @checkpoint_store = checkpoint_store
  @tool_trace_store = tool_trace_store
end

Instance Method Details

#assemble_tool_instances(allowed, state) ⇒ Object

Joins the direct instances (Policy/ToolAllowlist) with the capability-sourced ones (grant = profile.capabilities — they never went through Policy). Avoids double-exposure: if the SAME impl_name was also allowed directly, the DIRECT instance is discarded — the model sees only the capability alias.



59
60
61
62
63
64
65
66
67
68
69
# File 'lib/insika/tool_assembly.rb', line 59

def assemble_tool_instances(allowed, state)
  names = state.respond_to?(:capability_names) ? (state.capability_names || {}) : {}
  ctx = state.respond_to?(:turn_context) ? state.turn_context : nil
  return instantiate_tools(allowed, ctx) if names.empty?

  # Dedup by the ENTRY NAME (registry key = impl_name) BEFORE
  # instantiating — the INSTANCE's `.name` (RubyLLM) is not the registration
  # name.
  direct = Array(allowed).reject { |e| e.respond_to?(:name) && names.key?(e.name.to_s) }
  instantiate_tools(direct, ctx) + capability_tool_instances(names, ctx)
end

#resolve_capabilities(profile, context) ⇒ Object

Resolution sub-step BETWEEN Context and Policy — it does NOT feed candidate_tools (those stay ONLY tool_registry.entries, a capability does not go through the ToolAllowlist). Resolves each capability of the profile to the concrete Entry already registered in the tool_registry and keeps the impl_name -> capability_name mapping for the post-Policy join. Errors (Unavailable/Ambiguous, or an unregistered impl) propagate as a CapabilityError -> single capture in execute (stage :capability). Without



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/insika/tool_assembly.rb', line 36

def resolve_capabilities(profile, context)
  return {} if @capability_registry.nil?

  Array(profile.capabilities).each_with_object({}) do |cap_name, names|
    provider = @capability_registry.resolve(cap_name, profile: profile, context: context,
                                                       event_stream: @event_stream)
    next if provider.kind == :workflow # exposure to the agent loop is a follow-up

    entry = @tool_registry.entries.find { |e| e.name == provider.impl_name.to_s }
    if entry.nil?
      raise CapabilityError, "capability '#{cap_name}' resolveu para impl " \
                             "'#{provider.impl_name}', not registered in tool_registry"
    end

    names[entry.name] ||= cap_name.to_s # the 1st capability to claim an impl wins
  end
end

#wrap_tools(tools, state, skip_side_effects = []) ⇒ Object

Envelopes each allowed tool (per-call timeout + side-effect recording). The system LoadSkill (configure_chat) is NOT enveloped — it is a system tool with no side-effect and of trivial latency.



74
75
76
77
78
79
80
81
82
83
# File 'lib/insika/tool_assembly.rb', line 74

def wrap_tools(tools, state, skip_side_effects = [])
  timeout = state.profile.limits[:tool_timeout] || 60
  install_tool_gate(state)
  tools.map do |tool|
    ToolEnvelope.new(tool, state: state, checkpoint_store: @checkpoint_store,
                           tool_registry: @tool_registry, timeout: timeout,
                           skip_side_effects: skip_side_effects,
                           trace_recorder: @tool_trace_store)
  end
end