actionagent 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/assets/builds/action_agent.js +1 -1
- data/app/controllers/action_agent/api/agents_controller.rb +1 -1
- data/app/controllers/action_agent/dashboard_controller.rb +1 -1
- data/app/models/action_agent/agent.rb +47 -1
- data/app/services/action_agent/agent_toolbox.rb +24 -2
- data/app/services/action_agent/mcp_tool_dispatcher.rb +32 -0
- data/app/services/action_agent/scenario_evaluation_runner.rb +41 -2
- data/lib/action_agent/version.rb +1 -1
- data/lib/action_agent.rb +81 -0
- metadata +6 -3
|
@@ -41,7 +41,7 @@ module ActionAgent
|
|
|
41
41
|
providers: Agent::PROVIDERS,
|
|
42
42
|
presetTypes: Agent::PRESET_TYPES,
|
|
43
43
|
instructionSets: Agent::INSTRUCTION_SETS,
|
|
44
|
-
availableTools: Agent
|
|
44
|
+
availableTools: Agent.available_tools,
|
|
45
45
|
executionEnabled: ActionAgent.execution_enabled?,
|
|
46
46
|
assistantEnabled: ActionAgent.assistant_enabled?,
|
|
47
47
|
multiTenant: ActionAgent.multi_tenant?,
|
|
@@ -41,6 +41,7 @@ module ActionAgent
|
|
|
41
41
|
|
|
42
42
|
# Callbacks
|
|
43
43
|
before_validation :generate_slug, on: :create
|
|
44
|
+
before_validation :apply_conventional_schema_tools, on: :create
|
|
44
45
|
after_create :create_initial_version
|
|
45
46
|
after_update :create_version_on_config_change, if: :configuration_changed?
|
|
46
47
|
|
|
@@ -72,17 +73,62 @@ module ActionAgent
|
|
|
72
73
|
github ruby rails aws gcp python typescript docker kubernetes
|
|
73
74
|
].freeze
|
|
74
75
|
|
|
75
|
-
#
|
|
76
|
+
# Built-in tools/MCPs. Host-declared schema tools are offered alongside
|
|
77
|
+
# these — see .available_tools, which is what the editor and the APIs
|
|
78
|
+
# serialize. This constant stays the built-in set so existing references
|
|
79
|
+
# keep their meaning.
|
|
76
80
|
AVAILABLE_TOOLS = %w[
|
|
77
81
|
terminal playwright filesystem code database slack fetch search edit translate memory agents ui
|
|
78
82
|
].freeze
|
|
79
83
|
|
|
84
|
+
# Every tool an agent may enable: the built-ins plus each tool generated by
|
|
85
|
+
# the host's declared ActiveAgent::SchemaTools classes (ActionAgent.schema_tools).
|
|
86
|
+
#
|
|
87
|
+
# Computed per call, never memoized: in development the host's tool classes
|
|
88
|
+
# are autoloaded and reloaded, so a cached list would either miss them at
|
|
89
|
+
# boot or go stale after a reload.
|
|
90
|
+
# @return [Array<String>]
|
|
91
|
+
def self.available_tools
|
|
92
|
+
AVAILABLE_TOOLS | ActionAgent.schema_tool_names
|
|
93
|
+
end
|
|
94
|
+
|
|
80
95
|
# Available providers
|
|
81
96
|
PROVIDERS = %w[openai anthropic ollama openrouter].freeze
|
|
82
97
|
|
|
83
98
|
# The ActiveAgent class name this agent's runs are recorded under — the
|
|
84
99
|
# correlation key between platform Agent records and telemetry traces
|
|
85
100
|
# (TelemetryTrace#agent_class) and solid_agent contexts.
|
|
101
|
+
# Tools a schema tool class claims for this agent by naming convention:
|
|
102
|
+
# Reservation -> ReservationTools -> ReservationAgent.
|
|
103
|
+
#
|
|
104
|
+
# This is a DEFAULT SELECTION, never a restriction. Any agent may enable
|
|
105
|
+
# any tool in .available_tools; the convention only decides what a newly
|
|
106
|
+
# created ReservationAgent starts with.
|
|
107
|
+
# @return [Array<String>]
|
|
108
|
+
def conventional_schema_tools
|
|
109
|
+
# Compared on letters only. `telemetry_agent_class` is not reliable here:
|
|
110
|
+
# it runs `parameterize.camelize`, which turns an already-camelised
|
|
111
|
+
# "TicketAgent" into "Ticketagent" and matches nothing, while
|
|
112
|
+
# "Milestone Agent" happens to survive. Normalising both sides makes
|
|
113
|
+
# "TicketAgent", "Ticket Agent" and "ticket_agent" all match.
|
|
114
|
+
identifier = (agent_class_name.presence || name.to_s).gsub(/[^a-z]/i, "").downcase
|
|
115
|
+
return [] if identifier.blank?
|
|
116
|
+
|
|
117
|
+
ActionAgent.schema_tool_classes.select do |klass|
|
|
118
|
+
"#{klass.model.name}Agent".downcase == identifier
|
|
119
|
+
end.flat_map(&:tool_names).map(&:to_s)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Seeds a new agent named after a model with that model's tools. Only on
|
|
123
|
+
# create, and only when none were chosen — a deliberate selection, empty
|
|
124
|
+
# included, is never overwritten, and the editor can deselect afterwards.
|
|
125
|
+
def apply_conventional_schema_tools
|
|
126
|
+
return if tools.present?
|
|
127
|
+
|
|
128
|
+
defaults = conventional_schema_tools
|
|
129
|
+
self.tools = defaults if defaults.any?
|
|
130
|
+
end
|
|
131
|
+
|
|
86
132
|
def telemetry_agent_class
|
|
87
133
|
base = agent_class_name.presence || name.parameterize(separator: "_").camelize
|
|
88
134
|
base.end_with?("Agent") ? base : "#{base}Agent"
|
|
@@ -203,11 +203,23 @@ module ActionAgent
|
|
|
203
203
|
# Tool definitions for the subset of an agent's enabled tools that have
|
|
204
204
|
# server-side implementations.
|
|
205
205
|
def definitions_for(tool_names)
|
|
206
|
-
Array(tool_names).flat_map
|
|
206
|
+
Array(tool_names).flat_map do |name|
|
|
207
|
+
DEFINITIONS[name.to_s] || schema_tool_definitions(name.to_s)
|
|
208
|
+
end
|
|
207
209
|
end
|
|
208
210
|
|
|
209
211
|
def function?(name)
|
|
210
|
-
FUNCTIONS.key?(name.to_s)
|
|
212
|
+
FUNCTIONS.key?(name.to_s) || ActionAgent.schema_tool_class_for(name.to_s).present?
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Definitions for a host-declared schema tool, or [] when the name is
|
|
216
|
+
# not one. Each generated tool is its own entry so an agent enables them
|
|
217
|
+
# individually rather than as a group.
|
|
218
|
+
def schema_tool_definitions(name)
|
|
219
|
+
klass = ActionAgent.schema_tool_class_for(name)
|
|
220
|
+
return [] unless klass
|
|
221
|
+
|
|
222
|
+
Array(klass.tool_definitions).select { |definition| definition[:name].to_s == name }
|
|
211
223
|
end
|
|
212
224
|
|
|
213
225
|
# Executes a tool call. Returns a result hash; errors are returned as
|
|
@@ -218,6 +230,16 @@ module ActionAgent
|
|
|
218
230
|
# instead of re-running the side effect.
|
|
219
231
|
def call(name, **kwargs)
|
|
220
232
|
return { error: "Unknown tool: #{name}" } unless function?(name)
|
|
233
|
+
|
|
234
|
+
schema_tool = ActionAgent.schema_tool_class_for(name.to_s)
|
|
235
|
+
if schema_tool
|
|
236
|
+
# `actor:` is the host's authorization seam — the scope block runs
|
|
237
|
+
# inside the call. It is passed through untouched, including nil,
|
|
238
|
+
# so a host scope decides what an unattributed run may read rather
|
|
239
|
+
# than the engine widening it.
|
|
240
|
+
return schema_tool.call(name.to_s, actor: kwargs.delete(:actor), **kwargs)
|
|
241
|
+
end
|
|
242
|
+
|
|
221
243
|
return public_send(FUNCTIONS.fetch(name.to_s), **kwargs) if UNCACHED_FUNCTIONS.include?(name.to_s)
|
|
222
244
|
|
|
223
245
|
cached_fetch(name, kwargs) do
|
|
@@ -57,7 +57,15 @@ module ActionAgent
|
|
|
57
57
|
# contributes nothing rather than failing the run — the tools it serves
|
|
58
58
|
# then simply are not offered, and a scenario expecting them fails with a
|
|
59
59
|
# fault naming them.
|
|
60
|
+
#
|
|
61
|
+
# A server that fails discovery also records why, in +discovery_errors+.
|
|
62
|
+
# Contributing nothing keeps the run alive, but silence is indistinguishable
|
|
63
|
+
# from a server that legitimately serves no tools — and an agent offered no
|
|
64
|
+
# tools answers from the model alone, which reads as a confident, fabricated
|
|
65
|
+
# result rather than a transport failure (#425).
|
|
60
66
|
def tool_definitions
|
|
67
|
+
@discovery_errors = {}
|
|
68
|
+
|
|
61
69
|
resolver.declared_server_keys.flat_map do |key|
|
|
62
70
|
entry = MCPCatalog.find(key)
|
|
63
71
|
next [] unless entry && entry[:transport].to_s.in?(HTTP_TRANSPORTS) && entry[:url].present?
|
|
@@ -66,11 +74,35 @@ module ActionAgent
|
|
|
66
74
|
client_for(entry).list_tools
|
|
67
75
|
rescue MCPClient::Error => e
|
|
68
76
|
Rails.logger.warn("[MCPToolDispatcher] #{key} tools/list failed: #{e.message}")
|
|
77
|
+
@discovery_errors[key] =
|
|
78
|
+
"Cannot load tools from MCP server '#{key}' (#{entry[:url]}): #{e.message}. " \
|
|
79
|
+
"Check its URL, transport and credentials."
|
|
69
80
|
[]
|
|
70
81
|
end
|
|
71
82
|
end
|
|
72
83
|
end
|
|
73
84
|
|
|
85
|
+
# Why each declared server contributed no tools, keyed by server. Empty
|
|
86
|
+
# until +tool_definitions+ has run, and empty after a run where every
|
|
87
|
+
# declared server answered.
|
|
88
|
+
#
|
|
89
|
+
# @return [Hash{String => String}]
|
|
90
|
+
def discovery_errors
|
|
91
|
+
@discovery_errors ||= {}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Whether every server the agent declares failed discovery. The tool-less
|
|
95
|
+
# execution that follows cannot produce a meaningful result, so a caller
|
|
96
|
+
# can fail loudly instead of scoring an answer the model invented.
|
|
97
|
+
def all_servers_failed?
|
|
98
|
+
keys = resolver.declared_server_keys.select do |key|
|
|
99
|
+
entry = MCPCatalog.find(key)
|
|
100
|
+
entry && entry[:transport].to_s.in?(HTTP_TRANSPORTS) && entry[:url].present?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
keys.any? && keys.all? { |key| discovery_errors.key?(key) }
|
|
104
|
+
end
|
|
105
|
+
|
|
74
106
|
private
|
|
75
107
|
|
|
76
108
|
attr_reader :agent, :resolver
|
|
@@ -80,6 +80,7 @@ module ActionAgent
|
|
|
80
80
|
scores: scores_for(report, run),
|
|
81
81
|
samples_evaluated: report.results.size,
|
|
82
82
|
samples_passed: report.results.count(&:passed?),
|
|
83
|
+
error_message: discovery_warning,
|
|
83
84
|
completed_at: Time.current
|
|
84
85
|
)
|
|
85
86
|
run
|
|
@@ -240,10 +241,48 @@ module ActionAgent
|
|
|
240
241
|
end
|
|
241
242
|
end
|
|
242
243
|
|
|
244
|
+
# Every tool the agent could actually call, for the diagnosis roster: the
|
|
245
|
+
# engine's own toolbox plus whatever its MCP servers serve.
|
|
246
|
+
#
|
|
247
|
+
# Listing only the toolbox understated the roster, so a diagnosis could
|
|
248
|
+
# report "none of the available tools covers this task" while naming a list
|
|
249
|
+
# the agent's MCP tools were missing from.
|
|
243
250
|
def tool_roster
|
|
244
|
-
@tool_roster ||=
|
|
245
|
-
|
|
251
|
+
@tool_roster ||= begin
|
|
252
|
+
definitions = mcp_dispatcher.tool_definitions +
|
|
253
|
+
AgentToolbox.definitions_for(@evaluation.agent.tools)
|
|
254
|
+
|
|
255
|
+
definitions.to_h { |definition| [ definition[:name].to_s, definition[:description].to_s ] }
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Discovery failures from the roster above, keyed by server. Reading them
|
|
260
|
+
# requires tool_definitions to have run, which tool_roster does.
|
|
261
|
+
def mcp_discovery_errors
|
|
262
|
+
tool_roster
|
|
263
|
+
mcp_dispatcher.discovery_errors
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def mcp_dispatcher
|
|
267
|
+
@mcp_dispatcher ||= MCPToolDispatcher.new(@evaluation.agent)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# A run whose every declared MCP server failed discovery scored an agent
|
|
271
|
+
# that had no tools to call. The scores are real but meaningless — the
|
|
272
|
+
# model answered from its own weights — so the run says so rather than
|
|
273
|
+
# leaving the cause in a log line (#425).
|
|
274
|
+
def discovery_warning
|
|
275
|
+
errors = mcp_discovery_errors
|
|
276
|
+
return nil if errors.empty?
|
|
277
|
+
|
|
278
|
+
prefix = if mcp_dispatcher.all_servers_failed?
|
|
279
|
+
"Every MCP server this agent declares failed tool discovery, so it ran with no MCP tools " \
|
|
280
|
+
"and any specifics in its answers are unverified."
|
|
281
|
+
else
|
|
282
|
+
"Some of this agent's MCP servers failed tool discovery, so part of its toolset was unavailable."
|
|
246
283
|
end
|
|
284
|
+
|
|
285
|
+
"#{prefix} #{errors.values.join(' ')}"
|
|
247
286
|
end
|
|
248
287
|
|
|
249
288
|
# The judge the evaluation's owner has credentials for, wrapped for the
|
data/lib/action_agent/version.rb
CHANGED
data/lib/action_agent.rb
CHANGED
|
@@ -330,6 +330,30 @@ module ActionAgent
|
|
|
330
330
|
# @return [Array<Hash>]
|
|
331
331
|
attr_accessor :mcp_catalog
|
|
332
332
|
|
|
333
|
+
# Host-declared ActiveAgent::SchemaTools subclasses whose generated tools
|
|
334
|
+
# are offered alongside AgentToolbox's built-ins: each generated tool is
|
|
335
|
+
# individually selectable in the agent editor, dispatched by name at
|
|
336
|
+
# execution, and named in an evaluation's +tools:+ expectation.
|
|
337
|
+
#
|
|
338
|
+
# ActionAgent.configure do |config|
|
|
339
|
+
# config.schema_tools = [TicketTools, TaskTools, MilestoneTools]
|
|
340
|
+
# end
|
|
341
|
+
#
|
|
342
|
+
# Accepts classes or class-name strings; strings are resolved lazily so a
|
|
343
|
+
# host can declare them from an initializer before autoloading has run.
|
|
344
|
+
#
|
|
345
|
+
# Leave it unset and every ActiveAgent::SchemaTools subclass found in
|
|
346
|
+
# {#schema_tools_path} is offered instead — a host adds a tool by adding a
|
|
347
|
+
# file, without naming it twice.
|
|
348
|
+
# @return [Array<Class, String>, nil]
|
|
349
|
+
attr_accessor :schema_tools
|
|
350
|
+
|
|
351
|
+
# Directory scanned for SchemaTools subclasses when {#schema_tools} is
|
|
352
|
+
# unset. Relative to the host's root. Set to nil to disable discovery and
|
|
353
|
+
# require an explicit declaration.
|
|
354
|
+
# @return [String, nil]
|
|
355
|
+
attr_accessor :schema_tools_path
|
|
356
|
+
|
|
333
357
|
# Value stored in polymorphic *_type columns for dashboard agents
|
|
334
358
|
# (agent_memories.memorable_type, agent_contexts.contextable_type).
|
|
335
359
|
# Unset means the class name. A host app whose existing rows were
|
|
@@ -495,6 +519,63 @@ module ActionAgent
|
|
|
495
519
|
@sign_out_path = nil
|
|
496
520
|
@sign_in_path = nil
|
|
497
521
|
@mcp_catalog = []
|
|
522
|
+
@schema_tools = nil
|
|
523
|
+
@schema_tools_path = "app/agent_tools"
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
# Host-declared schema tool classes, resolved from names and filtered to
|
|
527
|
+
# those that are usable (declared a model and generated a roster).
|
|
528
|
+
#
|
|
529
|
+
# Resolution happens per call rather than at configure time: a host
|
|
530
|
+
# declares these in an initializer, before its own classes are autoloaded.
|
|
531
|
+
# @return [Array<Class>]
|
|
532
|
+
def schema_tool_classes
|
|
533
|
+
declared = @schema_tools.nil? ? discovered_schema_tools : Array(@schema_tools)
|
|
534
|
+
|
|
535
|
+
declared.filter_map do |entry|
|
|
536
|
+
klass = entry.is_a?(String) ? entry.safe_constantize : entry
|
|
537
|
+
next unless klass.respond_to?(:tool_definitions) && klass.respond_to?(:model)
|
|
538
|
+
next if klass.model.blank?
|
|
539
|
+
# An anonymous class built at runtime is usable but not discoverable —
|
|
540
|
+
# it would accumulate across reloads with no way to supersede itself.
|
|
541
|
+
next if entry.is_a?(Class) && klass.name.blank? && @schema_tools.nil?
|
|
542
|
+
|
|
543
|
+
klass
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
# Every tool name generated by the declared schema tool classes.
|
|
548
|
+
# @return [Array<String>]
|
|
549
|
+
def schema_tool_names
|
|
550
|
+
schema_tool_classes.flat_map(&:tool_names).map(&:to_s)
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
# SchemaTools subclasses defined under {#schema_tools_path}.
|
|
554
|
+
#
|
|
555
|
+
# The files are loaded before reading +descendants+: in development nothing
|
|
556
|
+
# has referenced those constants yet, so the list would otherwise be empty
|
|
557
|
+
# at boot and fill in only once something happened to touch them.
|
|
558
|
+
# @return [Array<Class>]
|
|
559
|
+
def discovered_schema_tools
|
|
560
|
+
return [] if @schema_tools_path.blank? || !defined?(ActiveAgent::SchemaTools)
|
|
561
|
+
return [] unless defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
562
|
+
|
|
563
|
+
root = Rails.root.join(@schema_tools_path)
|
|
564
|
+
return [] unless Dir.exist?(root)
|
|
565
|
+
|
|
566
|
+
Dir[root.join("**/*.rb")].sort.each do |path|
|
|
567
|
+
require_dependency path
|
|
568
|
+
rescue StandardError, ScriptError => e
|
|
569
|
+
warn "[ActionAgent] could not load #{path}: #{e.class} - #{e.message}"
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
ActiveAgent::SchemaTools.descendants.select { |klass| klass.name.present? }
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
# The schema tool class that generated +name+, or nil.
|
|
576
|
+
# @return [Class, nil]
|
|
577
|
+
def schema_tool_class_for(name)
|
|
578
|
+
schema_tool_classes.find { |klass| klass.tool?(name) }
|
|
498
579
|
end
|
|
499
580
|
end
|
|
500
581
|
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: actionagent
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.5.
|
|
4
|
+
version: 1.5.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Justin Bowen
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-09-11 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: activeagent
|
|
@@ -237,6 +238,7 @@ metadata:
|
|
|
237
238
|
documentation_uri: https://docs.activeagents.ai/framework/self-hosted-observability
|
|
238
239
|
source_code_uri: https://github.com/activeagents/activeagent
|
|
239
240
|
rubygems_mfa_required: 'true'
|
|
241
|
+
post_install_message:
|
|
240
242
|
rdoc_options: []
|
|
241
243
|
require_paths:
|
|
242
244
|
- lib
|
|
@@ -251,7 +253,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
251
253
|
- !ruby/object:Gem::Version
|
|
252
254
|
version: '0'
|
|
253
255
|
requirements: []
|
|
254
|
-
rubygems_version:
|
|
256
|
+
rubygems_version: 3.5.22
|
|
257
|
+
signing_key:
|
|
255
258
|
specification_version: 4
|
|
256
259
|
summary: The Active Agent dashboard, as a mountable Rails engine
|
|
257
260
|
test_files: []
|