actionagent 1.6.0 → 1.6.3

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.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/action_agent.js +45 -45
  3. data/app/controllers/action_agent/api/agent_runs_controller.rb +2 -2
  4. data/app/controllers/action_agent/api/agents_controller.rb +8 -5
  5. data/app/controllers/action_agent/api/analytics_controller.rb +1 -1
  6. data/app/controllers/action_agent/api/base_controller.rb +20 -0
  7. data/app/controllers/action_agent/api/interactions_controller.rb +3 -3
  8. data/app/controllers/action_agent/api/sandboxes_controller.rb +6 -1
  9. data/app/controllers/action_agent/api/session_recordings_controller.rb +24 -7
  10. data/app/models/action_agent/agent.rb +60 -0
  11. data/app/models/action_agent/agent_run.rb +4 -0
  12. data/app/models/action_agent/agent_version.rb +10 -0
  13. data/app/models/action_agent/evaluation.rb +19 -2
  14. data/app/models/action_agent/evaluation_run.rb +4 -0
  15. data/app/models/action_agent/telemetry_trace.rb +28 -1
  16. data/app/services/action_agent/agent_execution_service.rb +63 -12
  17. data/app/services/action_agent/agent_registrar.rb +23 -1
  18. data/app/services/action_agent/agent_release.rb +61 -0
  19. data/app/services/action_agent/agent_tool_roster.rb +47 -21
  20. data/app/services/action_agent/evaluation_runner_service.rb +12 -12
  21. data/app/services/action_agent/scenario_evaluation_runner.rb +16 -1
  22. data/config/routes.rb +3 -0
  23. data/lib/action_agent/version.rb +1 -1
  24. data/lib/generators/action_agent/install_generator.rb +6 -0
  25. data/lib/generators/action_agent/templates/action_agent.rb.erb +8 -0
  26. data/lib/generators/action_agent/templates/add_agent_releases.rb.erb +50 -0
  27. data/lib/tasks/action_agent.rake +33 -0
  28. metadata +8 -3
@@ -8,11 +8,19 @@ module ActionAgent
8
8
  # Three groups, one row vocabulary — the same one the Tools and MCP
9
9
  # Services pages use, so a roster reads the way the observability views do:
10
10
  #
11
- # * **Agent-defined** — tools discovered from the rosters this agent's own
12
- # generations offered (ToolDiscovery's +agent+ origin). Schema-derived:
13
- # the agent class declares them, so they are reported here rather than
14
- # selected. Read-only for the same reason MCP rows are — a checkbox that
15
- # cannot add or remove the tool is a control that changes nothing.
11
+ # * **Agent-defined** — the tools this agent's own generations offered
12
+ # (ToolDiscovery's +agent+ origin), plus every schema tool the host
13
+ # declares, on or off. Two kinds share the group. A *schema tool* — one
14
+ # a host ActiveAgent::SchemaTools class generates is offered only while
15
+ # +agent.tools+ names it: that is the reading AgentToolbox takes at
16
+ # generation time, and the evaluation runner, dashboard runs and the MCP
17
+ # facade all follow it. So it is selected here, and switchable, and every
18
+ # one has a row because any agent may enable any of them
19
+ # (Agent.available_tools) and a tool switched off has to keep its row to
20
+ # be switched back on. A tool the agent class declares *in code* is
21
+ # offered by the class itself; the dashboard reports it and cannot switch
22
+ # it, so its row is read-only — a checkbox that cannot add or remove the
23
+ # tool is a control that changes nothing.
16
24
  # * **Dashboard** — Agent::AVAILABLE_TOOLS, the capabilities the builder
17
25
  # offers every agent. These are the roster: +agent.tools+ is what
18
26
  # AgentToolbox turns into function schemas at generation time.
@@ -20,8 +28,9 @@ module ActionAgent
20
28
  # agent enables, which is where they are edited.
21
29
  #
22
30
  # Enablement reads the agent's own configuration: +tools+ for the dashboard
23
- # capabilities, +mcp_servers+ for services and their per-server allow-lists
24
- # (an entry with no +tools+ key offers everything the server serves).
31
+ # capabilities and the schema tools, +mcp_servers+ for services and their
32
+ # per-server allow-lists (an entry with no +tools+ key offers everything
33
+ # the server serves).
25
34
  class AgentToolRoster
26
35
  AGENT_DEFINED = "agent_defined"
27
36
  DASHBOARD = "dashboard"
@@ -158,23 +167,40 @@ module ActionAgent
158
167
  agent_defined_rows + dashboard_rows
159
168
  end
160
169
 
161
- # Schema-derived tools: whatever this agent's generations offered that
162
- # is neither an MCP tool nor one of the dashboard's own.
170
+ # Agent-defined tools: whatever this agent's generations offered that is
171
+ # neither an MCP tool nor one of the dashboard's own, plus every schema
172
+ # tool the host declares, whether or not the window saw it.
163
173
  def agent_defined_rows
164
- detected
174
+ observed = detected
165
175
  .select { |tool| tool[:origin] == ToolDiscovery::ORIGIN_AGENT }
166
176
  .reject { |tool| dashboard_function_names.include?(tool[:name]) }
167
- .map do |tool|
168
- usage_row(tool).merge(
169
- key: tool[:name],
170
- name: tool[:name],
171
- source: AGENT_DEFINED,
172
- description: tool[:description],
173
- # The agent class declares these; the dashboard reports them.
174
- enabled: true,
175
- editable: false
176
- )
177
- end
177
+ .index_by { |tool| tool[:name] }
178
+
179
+ (observed.keys + ActionAgent.schema_tool_names).uniq.map do |name|
180
+ tool = observed[name]
181
+ schema = schema_tool?(name)
182
+
183
+ usage_row(tool).merge(
184
+ key: name,
185
+ name: name,
186
+ source: AGENT_DEFINED,
187
+ description: tool&.dig(:description) || schema_tool_description(name),
188
+ # A schema tool is offered only while the roster names it — the
189
+ # window may still show it being called, but that is history. A
190
+ # tool the agent class declares in code is offered by the class.
191
+ enabled: schema ? saved_tools.include?(name) : true,
192
+ editable: schema
193
+ )
194
+ end
195
+ end
196
+
197
+ def schema_tool?(name)
198
+ @schema_tool ||= Hash.new { |cache, key| cache[key] = ActionAgent.schema_tool_class_for(key).present? }
199
+ @schema_tool[name]
200
+ end
201
+
202
+ def schema_tool_description(name)
203
+ AgentToolbox.schema_tool_definitions(name).first&.dig(:description)
178
204
  end
179
205
 
180
206
  def dashboard_rows
@@ -149,9 +149,7 @@ module ActionAgent
149
149
  end
150
150
 
151
151
  def sample_generations(model: nil)
152
- scope = AgentGeneration
153
- .joins(:agent_context)
154
- .where(AgentContext.table_name => { contextable: @evaluation.agent })
152
+ scope = @evaluation.agent.generations
155
153
  scope = scope.where(model: model) if model
156
154
  scope.order(created_at: :desc).limit(@evaluation.sample_size).to_a
157
155
  end
@@ -468,15 +466,17 @@ module ActionAgent
468
466
  end
469
467
 
470
468
  def judge_class
471
- provider = judge_provider
472
- model = @evaluation.judge_model.presence
473
- options = {}
474
- options[:model] = model if model
475
- options.merge!(owner_provider_options(provider))
476
-
477
- @judge_class ||= Class.new(ActiveAgent::Base) do
478
- define_singleton_method(:name) { "EvaluationJudgeAgent" }
479
- generate_with provider, **options
469
+ @judge_class ||= begin
470
+ provider = judge_provider
471
+ model = @evaluation.judge_model.presence
472
+ options = {}
473
+ options[:model] = model if model
474
+ options.merge!(owner_provider_options(provider))
475
+
476
+ Class.new(ActiveAgent::Base) do
477
+ define_singleton_method(:name) { "EvaluationJudgeAgent" }
478
+ generate_with provider, **options
479
+ end
480
480
  end
481
481
  end
482
482
 
@@ -64,7 +64,16 @@ module ActionAgent
64
64
  report = if adapter
65
65
  raise ArgumentError, "scenario evaluation adapter must be callable" unless adapter.respond_to?(:call)
66
66
 
67
- adapter.call(evaluation: @evaluation, owner: owner, scenarios: tasks, models: specs, on_result: on_result)
67
+ # The default path meters each replay itself (see #replay). An adapter
68
+ # runs the agent in the host's own runtime, out of reach of that call,
69
+ # so the execution is counted here as each result lands — one per
70
+ # scenario x model, the same unit. Otherwise a host that adapts the
71
+ # replay is silently unmetered unless it remembers to meter itself.
72
+ metered = lambda do |result|
73
+ ActionAgent.record_usage(owner, :execution)
74
+ on_result.call(result)
75
+ end
76
+ adapter.call(evaluation: @evaluation, owner: owner, scenarios: tasks, models: specs, on_result: metered)
68
77
  else
69
78
  ensure_judge_defined_kpis! if @evaluation.judge_defined?
70
79
  default_report(tasks, specs, on_result)
@@ -122,6 +131,12 @@ module ActionAgent
122
131
  def model_specs
123
132
  names = Array(@selection[:models]).presence || @evaluation.compare_models
124
133
  specs = Evals::ModelSpec.parse_all(names, default_provider: @evaluation.agent.provider, providers: Agent::PROVIDERS + %w[mock])
134
+ # parse_all resolves a bare name against `providers:` but passes through a
135
+ # `provider/model` whose provider is not in that list, so the run would
136
+ # otherwise reach the replay with a provider nothing can serve.
137
+ unsupported = specs.map(&:provider).uniq - (Agent::PROVIDERS + %w[mock])
138
+ raise ArgumentError, "unsupported model provider: #{unsupported.to_sentence}" if unsupported.any?
139
+
125
140
  return specs if specs.any?
126
141
 
127
142
  [ Evals::ModelSpec.new(label: @evaluation.agent.model, provider: @evaluation.agent.provider, model: @evaluation.agent.model) ]
data/config/routes.rb CHANGED
@@ -161,6 +161,9 @@ ActionAgent::Engine.routes.draw do
161
161
  # dashboard API key rather than a session, so it sits outside the api
162
162
  # namespace's session-authenticated controllers.
163
163
  post "mcp", to: "api/mcp#create"
164
+ match "mcp", to: ->(_env) { [ 405, { "Allow" => "POST" }, [] ] },
165
+ via: [ :get, :delete ],
166
+ constraints: ->(request) { request.delete? || !request.format.html? }
164
167
 
165
168
  # MCP Streamable HTTP (2025-03-26): a client MAY open the server-to-client
166
169
  # SSE stream with GET, and ends a session with DELETE. This facade offers
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionAgent
4
- VERSION = "1.6.0"
4
+ VERSION = "1.6.3"
5
5
  end
@@ -57,6 +57,12 @@ module ActionAgent
57
57
  )
58
58
  end
59
59
 
60
+ # Agent releases arrived after both tables shipped; the migration guards
61
+ # every column, so it is emitted for any install that lacks it.
62
+ unless existing_migration?("add_agent_releases")
63
+ migration_template("add_agent_releases.rb.erb", "db/migrate/add_agent_releases.rb")
64
+ end
65
+
60
66
  # The rest of the dashboard — agents, runs, versions, conversations,
61
67
  # evaluations, sandboxes, recordings, keys. Skippable for an app that
62
68
  # only wants to be a trace sink.
@@ -31,6 +31,14 @@ ActionAgent.configure do |config|
31
31
  # config.authentication_method = nil
32
32
  #
33
33
  config.authentication_method = nil
34
+ #
35
+ # Where a browser is sent when it asks for a dashboard page without a valid
36
+ # session; unset, it gets a minimal session-expired page. API clients get a
37
+ # bare 401 either way. The sign-out path is what the header's "Sign out"
38
+ # item POSTs to (with _method=delete); unset, the item is not shown.
39
+ #
40
+ # config.sign_in_path = "/sign_in"
41
+ # config.sign_out_path = "/sign_out"
34
42
 
35
43
  # ==========================================================================
36
44
  # Ingest API authentication
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Agent releases: a version cut from the agent's code carries the digest of
4
+ # what the model was given and the deploy it shipped in, and every trace,
5
+ # run and evaluation run records the version it ran under. Emitted for an
6
+ # install whose dashboard tables predate releases; the create-table
7
+ # migration carries the same columns for a fresh install.
8
+ #
9
+ # Each step is guarded so the migration is safe to re-run against a database
10
+ # that already has some of these columns.
11
+ class AddAgentReleases < ActiveRecord::Migration<%= migration_version %>
12
+ def up
13
+ add_column_unless_exists :active_agent_agents, :release_digest, :string
14
+
15
+ add_column_unless_exists :active_agent_agent_versions, :release_digest, :string
16
+ add_column_unless_exists :active_agent_agent_versions, :revision, :string
17
+ add_index_unless_exists :active_agent_agent_versions, [ :agent_id, :release_digest ]
18
+
19
+ %i[active_agent_telemetry_traces active_agent_agent_runs active_agent_evaluation_runs].each do |table|
20
+ add_column_unless_exists table, :agent_version_id, :bigint
21
+ add_index_unless_exists table, :agent_version_id
22
+ end
23
+ end
24
+
25
+ def down
26
+ remove_column :active_agent_agents, :release_digest if column_exists?(:active_agent_agents, :release_digest)
27
+ remove_column :active_agent_agent_versions, :release_digest if column_exists?(:active_agent_agent_versions, :release_digest)
28
+ remove_column :active_agent_agent_versions, :revision if column_exists?(:active_agent_agent_versions, :revision)
29
+
30
+ %i[active_agent_telemetry_traces active_agent_agent_runs active_agent_evaluation_runs].each do |table|
31
+ remove_column table, :agent_version_id if column_exists?(table, :agent_version_id)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def add_column_unless_exists(table, column, type)
38
+ return unless table_exists?(table)
39
+ return if column_exists?(table, column)
40
+
41
+ add_column table, column, type
42
+ end
43
+
44
+ def add_index_unless_exists(table, columns)
45
+ return unless table_exists?(table)
46
+ return if index_exists?(table, columns)
47
+
48
+ add_index table, columns
49
+ end
50
+ end
@@ -7,3 +7,36 @@ namespace :action_agent do
7
7
  puts "Agent template library: #{ActionAgent::AgentTemplate.count} templates"
8
8
  end
9
9
  end
10
+
11
+ namespace :action_agent do
12
+ namespace :agents do
13
+ desc "Cut a dashboard version for every agent whose code changed (run on deploy; REVISION=<git sha> to label it)"
14
+ task :release, [ :revision ] => :environment do |_task, args|
15
+ Rails.application.eager_load!
16
+ result = ActionAgent::AgentRelease.call(revision: args[:revision] || ENV["REVISION"], released_by: ENV["RELEASED_BY"])
17
+
18
+ puts "Release #{result.revision.presence || '(no revision)'}"
19
+ result.rows.each do |row|
20
+ if row.skipped
21
+ puts format(" %-28s skipped — %s", row.agent.name, row.skipped)
22
+ elsif row.cut
23
+ puts format(" %-28s v%-3d cut %s", row.agent.name, row.version.version_number, row.version.change_summary)
24
+ else
25
+ puts format(" %-28s v%-3d unchanged (%s)", row.agent.name, row.version.version_number, row.version.release_digest)
26
+ end
27
+ end
28
+ puts "#{result.cut.size} cut, #{result.rows.size - result.cut.size - result.skipped.size} unchanged, #{result.skipped.size} skipped"
29
+ end
30
+
31
+ desc "List each agent's current version and release"
32
+ task versions: :environment do
33
+ ActionAgent::Agent.order(:name).find_each do |agent|
34
+ version = agent.latest_version
35
+ release = agent.latest_release
36
+ puts format("%-28s v%-3s %s%s", agent.name, version&.version_number || "-",
37
+ release ? "release #{release.release_digest}" : "no release",
38
+ release&.revision.present? ? " · #{release.revision}" : "")
39
+ end
40
+ end
41
+ end
42
+ end
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.6.0
4
+ version: 1.6.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Bowen
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-09-18 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: activeagent
@@ -192,6 +193,7 @@ files:
192
193
  - app/serializers/action_agent/trace_interaction_serializer.rb
193
194
  - app/services/action_agent/agent_execution_service.rb
194
195
  - app/services/action_agent/agent_registrar.rb
196
+ - app/services/action_agent/agent_release.rb
195
197
  - app/services/action_agent/agent_scorecard.rb
196
198
  - app/services/action_agent/agent_tool_roster.rb
197
199
  - app/services/action_agent/agent_toolbox.rb
@@ -226,6 +228,7 @@ files:
226
228
  - lib/generators/action_agent/install_generator.rb
227
229
  - lib/generators/action_agent/templates/action_agent.rb.erb
228
230
  - lib/generators/action_agent/templates/add_agent_id_to_active_agent_telemetry_traces.rb.erb
231
+ - lib/generators/action_agent/templates/add_agent_releases.rb.erb
229
232
  - lib/generators/action_agent/templates/create_active_agent_dashboard_tables.rb.erb
230
233
  - lib/generators/action_agent/templates/create_active_agent_evaluation_scenarios.rb.erb
231
234
  - lib/generators/action_agent/templates/create_active_agent_telemetry_traces.rb.erb
@@ -238,6 +241,7 @@ metadata:
238
241
  documentation_uri: https://docs.activeagents.ai/framework/self-hosted-observability
239
242
  source_code_uri: https://github.com/activeagents/activeagent
240
243
  rubygems_mfa_required: 'true'
244
+ post_install_message:
241
245
  rdoc_options: []
242
246
  require_paths:
243
247
  - lib
@@ -252,7 +256,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
252
256
  - !ruby/object:Gem::Version
253
257
  version: '0'
254
258
  requirements: []
255
- rubygems_version: 3.6.9
259
+ rubygems_version: 3.5.22
260
+ signing_key:
256
261
  specification_version: 4
257
262
  summary: The Active Agent dashboard, as a mountable Rails engine
258
263
  test_files: []