Class: ActionAgent::ToolDiscovery

Inherits:
Object
  • Object
show all
Defined in:
app/services/action_agent/tool_discovery.rb

Overview

Builds the dashboard's tool and MCP-server inventory by reading what agents actually called, rather than what someone remembered to register.

Four record sources are merged, because each one sees a different slice of the same traffic:

  • The offered tool roster — the tools array in each generation request, recorded on the prompt span as prompt.input.tools and on each solid_agent generation as provenance["tools"]. The only source that sees a tool the model was given and never called, and the only one carrying descriptions and parameter names.
  • Telemetry tool spans — the only source for agents running in a host app and reporting in over the wire, and where timing and error rates come from.
  • solid_agent generations (+agent_generations.tool_calls+) — every tool call the model requested, including ones that never produced a span because the run died first.
  • solid_agent messages (+agent_messages+ with role: "tool") — the results that came back, which is where a tool's arguments survive even when telemetry is disabled.

Counting a tool once per source would multiply-count a dashboard-executed run, which writes all four. So each source feeds a distinct counter (+calls+, requested, results) and calls falls back to the largest observed count when telemetry is absent — a self-hosted install with telemetry off still gets real numbers instead of zeros.

MCP attribution comes from ActiveAgent::Telemetry::ToolOrigin (the mcp__server__tool convention, tagged onto spans at instrumentation time), then McpCatalog's hints for bare tool names, then the tool is treated as a method the agent class defines.

Scopes are passed in rather than derived, so the caller's ownership rules (single-user, per-user, or multi-tenant) decide what is visible.

Constant Summary collapse

DEFAULT_WINDOW_HOURS =
24 * 7
MAX_WINDOW_HOURS =
24 * 90
MAX_SCAN_ROWS =

The inventory is derived by reading records rather than by maintaining a summary table, and the window alone does not bound how many rows a busy account has in it — a 90-day window over a high-traffic workspace is millions. Scans stop here and say so rather than pinning a worker for the length of a request. A precomputed summary is the real answer; this keeps the page responsive until there is one.

20_000
ORIGIN_MCP =

Origin values, matching the framework's ToolOrigin plus the dashboard-only "builtin" bucket for AgentToolbox's executable tools.

"mcp"
ORIGIN_BUILTIN =
"builtin"
ORIGIN_AGENT =
"agent"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(traces:, agents:, hours: DEFAULT_WINDOW_HOURS) ⇒ ToolDiscovery

Returns a new instance of ToolDiscovery.

Parameters:

  • traces (ActiveRecord::Relation)

    the traces the caller may read

  • agents (ActiveRecord::Relation)

    the agents the caller may reach

  • hours (Integer) (defaults to: DEFAULT_WINDOW_HOURS)

    how far back to look



61
62
63
64
65
66
# File 'app/services/action_agent/tool_discovery.rb', line 61

def initialize(traces:, agents:, hours: DEFAULT_WINDOW_HOURS)
  @traces = traces
  @agents = agents
  @window_hours = hours.to_i.clamp(1, MAX_WINDOW_HOURS)
  @since = @window_hours.hours.ago
end

Instance Attribute Details

#agentsObject (readonly)

Returns the value of attribute agents.



56
57
58
# File 'app/services/action_agent/tool_discovery.rb', line 56

def agents
  @agents
end

#sinceObject (readonly)

Returns the value of attribute since.



56
57
58
# File 'app/services/action_agent/tool_discovery.rb', line 56

def since
  @since
end

#tracesObject (readonly)

Returns the value of attribute traces.



56
57
58
# File 'app/services/action_agent/tool_discovery.rb', line 56

def traces
  @traces
end

#window_hoursObject (readonly)

Returns the value of attribute window_hours.



56
57
58
# File 'app/services/action_agent/tool_discovery.rb', line 56

def window_hours
  @window_hours
end

Class Method Details

.builtin_toolsObject

Tools AgentToolbox implements inside the dashboard. Neither MCP nor agent-defined — the engine runs them — so they get their own origin rather than being mislabeled as methods on the agent class. Resolved lazily: referencing an autoloaded constant while this class is being defined would bind whatever happened to be loaded first.



111
112
113
114
# File 'app/services/action_agent/tool_discovery.rb', line 111

def self.builtin_tools
  @builtin_tools ||= AgentToolbox::DEFINITIONS.values.flatten
    .map { |definition| definition[:name].to_s }.to_set
end

Instance Method Details

#detected_toolsArray<Hash>

Detected tools, most-used first.

Returns:

  • (Array<Hash>)


86
87
88
89
90
91
92
93
94
95
96
# File 'app/services/action_agent/tool_discovery.rb', line 86

def detected_tools
  index = {}

  merge_declared_tools(index)
  merge_trace_tools(index)
  merge_generation_tools(index)
  merge_message_tools(index)
  merge_configured_tools(index)

  index.values.map { |entry| finalize(entry) }.sort_by { |tool| [ -tool[:calls], tool[:name] ] }
end

#inventoryHash

The full inventory: every tool seen in the window, plus the MCP servers they roll up into and the configured-but-unused surface.

Returns:

  • (Hash)


72
73
74
75
76
77
78
79
80
81
# File 'app/services/action_agent/tool_discovery.rb', line 72

def inventory
  tools = detected_tools
  {
    tools: tools,
    servers: servers_for(tools),
    summary: summary_for(tools),
    window_hours: window_hours,
    sources: source_availability
  }
end

#servers(tools = detected_tools) ⇒ Array<Hash>

MCP servers, detected traffic joined against the catalog so unused defaults are still listed (as status: "available").

Returns:

  • (Array<Hash>)


102
103
104
# File 'app/services/action_agent/tool_discovery.rb', line 102

def servers(tools = detected_tools)
  servers_for(tools)
end