Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,31 @@ jobs:
- name: Publish the framework, then the dashboard that depends on it
# actionagent declares activeagent >= 1.2, so publishing it first
# would leave a release nobody can bundle until the framework lands.
#
# A version already on RubyGems is skipped rather than fatal: the two
# gems share this tag but version independently, so a release that
# patches only one of them rebuilds the other at its current version.
# Without the skip, `gem push` rejects that duplicate and takes the
# gem that actually changed down with it.
run: |
gem push pkg/activeagent-*.gem
gem push pkg/actionagent-*.gem
publish() {
local gem_path
gem_path=$(ls $1)
local name version
name=$(basename "$gem_path" .gem | sed -E 's/-[0-9]+\.[0-9]+\.[0-9]+.*$//')
version=$(basename "$gem_path" .gem | sed -E "s/^${name}-//")

# `gem list --all` prints one line: `name (1.2.0, 1.1.0, ...)`, so the
# version is matched between its delimiters — anchoring on "($version"
# alone would only ever see the newest release.
if gem list --remote --exact --all "$name" | tr -d ' ' |
grep -qE "[(,]${version//./\\.}[,)]"; then
echo "$name $version is already on RubyGems; skipping."
return 0
fi

gem push "$gem_path"
}

publish "pkg/activeagent-*.gem"
publish "pkg/actionagent-*.gem"
24 changes: 23 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,29 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.2.0] - Unreleased
## [1.2.1] - 2026-08-14

### Fixed

- **`actionagent`: the install migrations now run on MySQL.** Both templates
already chose the JSON column type per adapter, but kept `default: []` /
`default: {}` for every adapter, and MySQL rejects a default on a JSON
column outright — so `rails g action_agent:install && rails db:migrate`
aborted mid-`create_table` on any MySQL host. The default (and the paired
`null: false`, which without it would reject the inserts the default
existed to satisfy) is now PostgreSQL-only. Every JSON column is read
through `Array(...)` / `|| {}`, so a NULL reads as the empty value.
- **`actionagent`: the mount works in a host that declares
`inflect.acronym "API"`.** An engine's files are autoloaded under the
host's inflections, so such a host made Zeitwerk expect
`ActionAgent::API::TracesController` from a file defining
`ActionAgent::Api::TracesController`, and every request to the mount
raised `Zeitwerk::NameError`. Rails separately camelizes a route's stored
controller path with the host's global inflections, which no engine-level
setting scopes. The autoloader is now pinned to `Api` for this engine's
own path, and the namespace answers to `API` as well.

## [1.2.0] - 2026-08-14

### ⚠️ The dashboard has moved to its own gem

Expand Down
41 changes: 41 additions & 0 deletions actionagent/lib/action_agent/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,47 @@ class Engine < ::Rails::Engine
app.config.filter_parameters += [ :credential, :api_key, :access_token ]
end

# The controllers under app/controllers/action_agent/api are ActionAgent::Api,
# but an engine's files are autoloaded by the host's `rails.main` loader,
# under the host's inflections. A host that declares `inflect.acronym "API"`
# — common enough that Rails documents it — makes Zeitwerk expect
# ActionAgent::API::TracesController in a file that defines
# ActionAgent::Api::TracesController, and every request to the mount raises
# Zeitwerk::NameError.
#
# Scoped to this engine's own path rather than set through `inflect`: the
# loader is shared, so a blanket rule would re-spell the host's own API
# constants. `camelize` receives the absolute path, which is the only hook
# that can tell this engine's api/ from the host's.
initializer "action_agent.inflections", before: :set_autoload_paths do
engine_root = root.to_s

Rails.autoloaders.main.inflector.singleton_class.prepend(Module.new do
define_method(:camelize) do |basename, abspath|
next "Api" if basename == "api" && abspath.to_s.start_with?(engine_root)

super(basename, abspath)
end
end)
end

# Rails resolves a route's controller by camelizing the stored path
# ("action_agent/api/traces") with the host's *global* inflections, and no
# engine-level setting scopes that — so an acronym host looks up
# ActionAgent::API::TracesController and raises NameError even though the
# autoloader named the module correctly above. No single spelling satisfies
# both kinds of host: a plain host camelizes to Api, an acronym host to API.
# So the namespace answers to both. `const_missing` rather than an eager
# alias because the controllers are autoloaded on demand, and naming them at
# boot would load the whole dashboard.
ActionAgent.singleton_class.prepend(Module.new do
def const_missing(name)
return const_get(:Api) if name == :API

super
end
end)

initializer "action_agent.assets", before: :append_assets_path do |app|
builds = root.join("app", "assets", "builds").to_s
next unless File.directory?(builds)
Expand Down
2 changes: 1 addition & 1 deletion actionagent/lib/action_agent/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module ActionAgent
VERSION = "1.2.0"
VERSION = "1.2.1"
end
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ class CreateActiveAgentDashboardTables < ActiveRecord::Migration<%= migration_ve
t.string :agent_class_name
t.string :action_name
t.integer :status, default: 0, null: false
t.column :appearance, json_type, default: {}
t.column :instruction_sets, json_type, default: []
t.column :tools, json_type, default: []
t.column :mcp_servers, json_type, default: []
t.column :model_config, json_type, default: {}
t.column :response_format, json_type, default: {}
t.column :action_prompts, json_type, default: [], null: false
t.column :appearance, json_type, **json_default({})
t.column :instruction_sets, json_type, **json_default([])
t.column :tools, json_type, **json_default([])
t.column :mcp_servers, json_type, **json_default([])
t.column :model_config, json_type, **json_default({})
t.column :response_format, json_type, **json_default({})
t.column :action_prompts, json_type, **json_default([], null: false)

# Agents discovered from reported telemetry rather than authored here.
t.string :service_name
Expand All @@ -52,7 +52,7 @@ class CreateActiveAgentDashboardTables < ActiveRecord::Migration<%= migration_ve
create_table "#{prefix}agent_versions" do |t|
t.bigint :agent_id, null: false
t.integer :version_number, default: 1, null: false
t.column :configuration_snapshot, json_type, default: {}, null: false
t.column :configuration_snapshot, json_type, **json_default({}, null: false)
t.string :change_summary
t.string :created_by
t.timestamps
Expand All @@ -65,10 +65,10 @@ class CreateActiveAgentDashboardTables < ActiveRecord::Migration<%= migration_ve
t.string :action_name
t.integer :status, default: 0, null: false
t.text :input_prompt
t.column :input_params, json_type, default: {}
t.column :input_params, json_type, **json_default({})
t.text :output
t.column :output_metadata, json_type, default: {}
t.column :logs, json_type, default: []
t.column :output_metadata, json_type, **json_default({})
t.column :logs, json_type, **json_default([])
t.text :error_message
t.text :error_backtrace
t.integer :input_tokens
Expand All @@ -93,11 +93,11 @@ class CreateActiveAgentDashboardTables < ActiveRecord::Migration<%= migration_ve
t.string :model, default: "gpt-4o-mini"
t.text :instructions
t.string :preset_type
t.column :appearance, json_type, default: {}
t.column :instruction_sets, json_type, default: []
t.column :tools, json_type, default: []
t.column :mcp_servers, json_type, default: {}
t.column :model_config, json_type, default: {}
t.column :appearance, json_type, **json_default({})
t.column :instruction_sets, json_type, **json_default([])
t.column :tools, json_type, **json_default([])
t.column :mcp_servers, json_type, **json_default({})
t.column :model_config, json_type, **json_default({})
t.boolean :featured, default: false
t.boolean :free_tier, default: false
t.boolean :public, default: true
Expand All @@ -114,7 +114,7 @@ class CreateActiveAgentDashboardTables < ActiveRecord::Migration<%= migration_ve
t.string :contextable_type
t.bigint :contextable_id
t.text :instructions
t.column :options, json_type, default: {}
t.column :options, json_type, **json_default({})
t.string :trace_id
t.integer :total_input_tokens, default: 0
t.integer :total_output_tokens, default: 0
Expand All @@ -130,11 +130,11 @@ class CreateActiveAgentDashboardTables < ActiveRecord::Migration<%= migration_ve
t.string :content_checksum
t.string :tool_call_id
t.string :tool_name
t.column :tool_arguments, json_type, default: {}
t.column :tool_arguments, json_type, **json_default({})
t.column :tool_result, json_type
t.column :attachments, json_type, default: []
t.column :metadata, json_type, default: {}
t.column :provenance, json_type, default: {}
t.column :attachments, json_type, **json_default([])
t.column :metadata, json_type, **json_default({})
t.column :provenance, json_type, **json_default({})
t.timestamps
t.index :agent_context_id
end
Expand All @@ -150,9 +150,9 @@ class CreateActiveAgentDashboardTables < ActiveRecord::Migration<%= migration_ve
t.integer :cached_tokens, default: 0
t.integer :reasoning_tokens, default: 0
t.float :duration_seconds
t.column :tool_calls, json_type, default: []
t.column :tool_calls, json_type, **json_default([])
t.column :raw_response, json_type
t.column :provenance, json_type, default: {}
t.column :provenance, json_type, **json_default({})
t.string :trace_id
t.timestamps
t.index :agent_context_id
Expand Down Expand Up @@ -183,16 +183,16 @@ class CreateActiveAgentDashboardTables < ActiveRecord::Migration<%= migration_ve
t.string :judge_kind, default: "rules", null: false
t.string :judge_model
t.integer :sample_size, default: 20, null: false
t.column :criteria, json_type, default: [], null: false
t.column :config, json_type, default: {}, null: false
t.column :criteria, json_type, **json_default([], null: false)
t.column :config, json_type, **json_default({}, null: false)
t.timestamps
t.index [ :agent_id, :name ], unique: true
end

create_table "#{prefix}evaluation_runs" do |t|
t.bigint :evaluation_id, null: false
t.integer :status, default: 0, null: false
t.column :scores, json_type, default: {}
t.column :scores, json_type, **json_default({})
t.integer :samples_evaluated, default: 0
t.integer :samples_passed, default: 0
t.text :error_message
Expand All @@ -208,11 +208,11 @@ class CreateActiveAgentDashboardTables < ActiveRecord::Migration<%= migration_ve
t.string :sandbox_type, default: "playwright_mcp"
t.string :cloud_run_job_id
t.string :cloud_run_url
t.column :runs, json_type, default: []
t.column :runs, json_type, **json_default([])
# MCP servers this session was started with, so the MCP Services view
# can show a launched server as running rather than offering to start
# a second copy.
t.column :mcp_servers, json_type, default: []
t.column :mcp_servers, json_type, **json_default([])
t.integer :runs_count, default: 0
t.integer :max_runs, default: 10
t.integer :timeout_seconds, default: 300
Expand All @@ -234,7 +234,7 @@ class CreateActiveAgentDashboardTables < ActiveRecord::Migration<%= migration_ve
t.integer :status, default: 0, null: false
t.text :result
t.text :error
t.column :screenshots, json_type, default: []
t.column :screenshots, json_type, **json_default([])
t.integer :tokens_used, default: 0
t.integer :duration_ms
t.datetime :started_at
Expand All @@ -250,7 +250,7 @@ class CreateActiveAgentDashboardTables < ActiveRecord::Migration<%= migration_ve
t.integer :status, default: 0, null: false
t.integer :action_count, default: 0
t.integer :duration_ms
t.column :metadata, json_type, default: {}
t.column :metadata, json_type, **json_default({})
t.bigint :user_id
t.bigint :account_id
t.timestamps
Expand All @@ -269,7 +269,7 @@ class CreateActiveAgentDashboardTables < ActiveRecord::Migration<%= migration_ve
t.integer :timestamp_ms, null: false
t.string :screenshot_key
t.string :dom_snapshot_key
t.column :metadata, json_type, default: {}
t.column :metadata, json_type, **json_default({})
t.timestamps
t.index [ :session_recording_id, :sequence ], unique: true,
name: "index_active_agent_recording_actions_on_recording_and_sequence"
Expand Down Expand Up @@ -314,6 +314,21 @@ class CreateActiveAgentDashboardTables < ActiveRecord::Migration<%= migration_ve

# jsonb where the adapter has it, json where it doesn't.
def json_type
@json_type ||= connection.adapter_name.to_s.downcase.include?("postgres") ? :jsonb : :json
@json_type ||= postgres? ? :jsonb : :json
end

# MySQL rejects a default on a JSON column outright ("BLOB, TEXT, GEOMETRY or
# JSON column can't have a default value"), which aborts create_table, so the
# column is created without one there. `null: false` is dropped with it:
# nothing assigns these before validation, so a NOT NULL column with no
# default would reject the very inserts the default was there to satisfy.
def json_default(value, null: nil)
return {} unless postgres?

null.nil? ? { default: value } : { default: value, null: null }
end

def postgres?
connection.adapter_name.to_s.downcase.include?("postgres")
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ class CreateActiveAgentTelemetryTraces < ActiveRecord::Migration<%= migration_ve
t.string :service_name
t.string :environment
t.datetime :timestamp, index: true
t.column :spans, json_type, default: []
t.column :resource_attributes, json_type, default: {}
t.column :sdk_info, json_type, default: {}
t.column :spans, json_type, **json_default([])
t.column :resource_attributes, json_type, **json_default({})
t.column :sdk_info, json_type, **json_default({})
t.decimal :total_duration_ms, precision: 12, scale: 3
t.integer :total_input_tokens, default: 0
t.integer :total_output_tokens, default: 0
Expand Down Expand Up @@ -53,6 +53,19 @@ class CreateActiveAgentTelemetryTraces < ActiveRecord::Migration<%= migration_ve
# traverses spans in SQL on PostgreSQL, and jsonb_array_elements rejects a
# json argument, so the column type and the query have to agree.
def json_type
@json_type ||= connection.adapter_name.to_s.downcase.include?("postgres") ? :jsonb : :json
@json_type ||= postgres? ? :jsonb : :json
end

# MySQL rejects a default on a JSON column outright ("BLOB, TEXT, GEOMETRY or
# JSON column can't have a default value"), which aborts create_table, so the
# column is created without one there. ActionAgent::TelemetryTrace reads every
# JSON column through `Array(...)`/`|| {}`, and ingest always assigns all
# three, so a NULL reads the same as the empty default would.
def json_default(value)
postgres? ? { default: value } : {}
end

def postgres?
connection.adapter_name.to_s.downcase.include?("postgres")
end
end