actionagent 1.2.0 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 82a881aeb6aa97004b573137a8afdd5d61ac5d60f8d8cc1b28ca16e3712ec4c1
4
- data.tar.gz: ccaba27a88b3297711515bfd8714e4449a3715c388af11874bfc60a63eaf3228
3
+ metadata.gz: f6e31cd17a84e6621881630137c3478268d2e31c8bba138516a397c23964c4f9
4
+ data.tar.gz: 28e83e7b84704c5ff9de2b475998273ac3dd328929515472f31e3743981927de
5
5
  SHA512:
6
- metadata.gz: fef536ffb278c2d0a9b93dbf52a689de927b3b0c9dfd572ed68c0e3fac5a06bf661bd130151094c45c9c722ef093d641c8bbbd3b7d7c1c84b789ea4b88071803
7
- data.tar.gz: abfbab6a0e0251233da133a0ce00572997ecd5ee8b3db952e6a854cb1b9b8f94e4182ac9976712b4457cc38d70b02cb4797b5b0b60ec61c2fa32100b631728fe
6
+ metadata.gz: 3a9e49dce9507b1243dd494b842c967bb5cb04921a7825ae2e16e37ed3cf25e845ae5b86de26b50cacdb01e7120859624ea7667d05c0d04048489bc7af5b534e
7
+ data.tar.gz: bebd359520c4ea07d69288ef8381afd5e5f663ea6df3264e17db81c2bc1a6d78836be216a8677666ede6223a1069f39e3e8e3a709025adcf8c9817a2be9750c9
@@ -135,7 +135,7 @@ module ActionAgent
135
135
  # Add account if in multi-tenant mode
136
136
  attrs[:account] = account if ActionAgent.multi_tenant? && account
137
137
 
138
- create!(attrs)
138
+ create!(attrs).tap { |record| AgentRegistrar.call(record) }
139
139
  end
140
140
 
141
141
  # Sums a span's token counts (used to decide which spans carry the
@@ -354,8 +354,14 @@ module ActionAgent
354
354
  define_singleton_method(:name) { klass_name }
355
355
 
356
356
  # Persist the conversation (agent_contexts / agent_messages /
357
- # agent_generations) via solid_agent. contextable: false the context
358
- # is loaded explicitly in the action below.
357
+ # agent_generations) via solid_agent. Auto-context is switched off
358
+ # the context is loaded explicitly in the action below.
359
+ #
360
+ # The keyword that switches it off was renamed (contextable: ->
361
+ # contextual:) between solid_agent 0.1 and 0.2, and the gemspec floor
362
+ # admits both, so it is resolved from the installed method rather than
363
+ # hard-coded: passing the wrong one is an ArgumentError that only
364
+ # surfaces when a run executes.
359
365
  #
360
366
  # The model classes are named explicitly because solid_agent infers
361
367
  # bare "AgentContext"/"AgentMessage"/"AgentGeneration" and resolves
@@ -363,10 +369,12 @@ module ActionAgent
363
369
  # inferred names only resolve in a host app that happens to have
364
370
  # top-level models of its own.
365
371
  include SolidAgent::HasContext
366
- has_context contextable: false,
372
+ has_context(
373
+ ActionAgent.solid_agent_auto_context_keyword => false,
367
374
  class_name: "ActionAgent::AgentContext",
368
375
  message_class: "ActionAgent::AgentMessage",
369
376
  generation_class: "ActionAgent::AgentGeneration"
377
+ )
370
378
 
371
379
  if effective_provider == :mock
372
380
  # Test environment only (see #provider_available?).
@@ -399,7 +399,7 @@ module ActionAgent
399
399
  if defined?(SolidAgent::ToolCache)
400
400
  SolidAgent::ToolCache.fetch(tool: name.to_s, args: kwargs, ttl: CACHE_TTL, &block)
401
401
  else
402
- key = "solid_agent:tool_cache:#{name}:#{Digest::SHA256.hexdigest(kwargs.sort.to_h.to_json)}"
402
+ key = fallback_cache_key(name, kwargs)
403
403
  cached = Rails.cache.read(key)
404
404
  return cached.merge(cached: true) unless cached.nil?
405
405
 
@@ -411,6 +411,29 @@ module ActionAgent
411
411
  end
412
412
  end
413
413
 
414
+ # Byte-for-byte the key SolidAgent::ToolCache would compute, so an app
415
+ # that upgrades solid_agent mid-TTL keeps reading what it already
416
+ # cached instead of silently starting over. Nested hashes and
417
+ # symbol/string keys have to normalize the same way, which a plain
418
+ # `kwargs.sort.to_h.to_json` does not do.
419
+ #
420
+ # test/integration/solid_agent/tool_cache_test.rb asserts the two
421
+ # schemes still agree.
422
+ def fallback_cache_key(name, kwargs)
423
+ "solid_agent:tool_cache:#{name}:#{Digest::SHA256.hexdigest(normalize_cache_args(kwargs).to_json)}"
424
+ end
425
+
426
+ def normalize_cache_args(args)
427
+ case args
428
+ when Hash
429
+ args.map { |key, value| [ key.to_s, normalize_cache_args(value) ] }.sort_by(&:first)
430
+ when Array
431
+ args.map { |value| normalize_cache_args(value) }
432
+ else
433
+ args
434
+ end
435
+ end
436
+
414
437
  # SSRF guard for fetch_url: reject hosts that resolve to loopback,
415
438
  # private, or link-local addresses.
416
439
  def public_host?(host)
@@ -13,6 +13,13 @@ module ActionAgent
13
13
 
14
14
  engine_name "action_agent"
15
15
 
16
+ # Basenames this engine spells differently from Zeitwerk's default
17
+ # camelization, consulted by the inflections initializer below. Empty
18
+ # today: every file here is named for the constant default camelization
19
+ # produces. An engine file that wants a genuine acronym in its constant
20
+ # adds its basename here rather than relying on the host to register one.
21
+ INFLECTION_OVERRIDES = {}.freeze
22
+
16
23
  config.action_agent = ActiveSupport::OrderedOptions.new
17
24
 
18
25
  # The dashboard's JS and CSS ship prebuilt in the gem. Adding the
@@ -26,6 +33,75 @@ module ActionAgent
26
33
  app.config.filter_parameters += [ :credential, :api_key, :access_token ]
27
34
  end
28
35
 
36
+ # This engine's constants are spelled the way Zeitwerk's own inflector
37
+ # spells them — Api, McpCatalog, ApiKey — but an engine's files are
38
+ # autoloaded by the host's `rails.main` loader, under the *host's*
39
+ # inflections. A host that declares `inflect.acronym "API"` or "MCP" (both
40
+ # common, and documented by Rails) makes Zeitwerk expect
41
+ # ActionAgent::API::TracesController or ActionAgent::MCPCatalog from files
42
+ # that define ActionAgent::Api::TracesController and
43
+ # ActionAgent::McpCatalog. The constant never resolves and the request
44
+ # raises Zeitwerk::NameError.
45
+ #
46
+ # Every path under this engine therefore camelizes with Zeitwerk's default
47
+ # rules, ignoring whatever acronyms the host has registered. Applied by
48
+ # path rather than through `inflect`: the loader is shared with the host,
49
+ # so a blanket rule would re-spell the host's own constants. `camelize`
50
+ # receives the absolute path, which is the only hook that can tell this
51
+ # engine's files from the host's.
52
+ #
53
+ # Basenames whose spelling this engine cannot express through default
54
+ # camelization (a genuine acronym it wants uppercased) go in OVERRIDES.
55
+ initializer "action_agent.inflections", before: :set_autoload_paths do
56
+ engine_root = File.join(root.to_s, "")
57
+ default = Zeitwerk::Inflector.new
58
+
59
+ Rails.autoloaders.main.inflector.singleton_class.prepend(Module.new do
60
+ define_method(:camelize) do |basename, abspath|
61
+ next super(basename, abspath) unless abspath.to_s.start_with?(engine_root)
62
+
63
+ ActionAgent::Engine::INFLECTION_OVERRIDES.fetch(basename) { default.camelize(basename, abspath) }
64
+ end
65
+ end)
66
+ end
67
+
68
+ # Rails resolves a route's controller by camelizing the stored path
69
+ # ("action_agent/api/traces") with the host's *global* inflections, and no
70
+ # engine-level setting scopes that — so an acronym host looks up
71
+ # ActionAgent::API::TracesController and raises NameError even though the
72
+ # autoloader named the module correctly above. No single spelling satisfies
73
+ # both kinds of host: a plain host camelizes to Api, an acronym host to API.
74
+ # So the namespace answers to both. `const_missing` rather than an eager
75
+ # alias because the controllers are autoloaded on demand, and naming them at
76
+ # boot would load the whole dashboard.
77
+ # The router does not consult the autoloader's inflector, so an acronym
78
+ # host asks for ActionAgent::API::MCPServersController while the constants
79
+ # are Api::McpServersController. Rather than enumerate the pairs, an
80
+ # all-caps run in a missing constant is retried in the spelling default
81
+ # camelization produces: API -> Api, MCPServersController -> McpServers-
82
+ # Controller. Only the engine's own namespaces are touched, and only for a
83
+ # constant that is already missing.
84
+ inflection_shim = Module.new do
85
+ def const_missing(name)
86
+ relaxed = name.to_s.gsub(/([A-Z])([A-Z]+)(?=[A-Z][a-z]|\d|\z)/) { "#{$1}#{$2.downcase}" }
87
+
88
+ return super if relaxed == name.to_s || !const_defined?(relaxed, false)
89
+
90
+ const_get(relaxed, false)
91
+ end
92
+ end
93
+
94
+ ActionAgent.singleton_class.prepend(inflection_shim)
95
+
96
+ # ActionAgent::Api is autoloaded, so it cannot be reopened at this point.
97
+ # Zeitwerk hands it over as soon as it is defined, which is before the
98
+ # router can ask it for a controller.
99
+ initializer "action_agent.api_inflection_shim" do
100
+ Rails.autoloaders.main.on_load("ActionAgent::Api") do |mod, _abspath|
101
+ mod.singleton_class.prepend(inflection_shim)
102
+ end
103
+ end
104
+
29
105
  initializer "action_agent.assets", before: :append_assets_path do |app|
30
106
  builds = root.join("app", "assets", "builds").to_s
31
107
  next unless File.directory?(builds)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionAgent
4
- VERSION = "1.2.0"
4
+ VERSION = "1.2.2"
5
5
  end
data/lib/action_agent.rb CHANGED
@@ -20,6 +20,25 @@ module ActionAgent
20
20
  global = defined?(::ActiveRecord::Base) ? ::ActiveRecord::Base.table_name_prefix : ""
21
21
  "#{global}#{@table_name_prefix ||= "active_agent_"}"
22
22
  end
23
+
24
+ # Which keyword the installed solid_agent uses to switch has_context's
25
+ # auto-context off: `contextable:` up to 0.1, `contextual:` from 0.2. The
26
+ # gemspec floor admits both, and passing the wrong one raises an
27
+ # ArgumentError deep inside a run rather than at boot — so
28
+ # AgentExecutionService asks rather than assumes.
29
+ #
30
+ # Covered by test/integration/solid_agent, which runs this engine against
31
+ # solid_agent's main branch as well as the released gem.
32
+ def solid_agent_auto_context_keyword
33
+ @solid_agent_auto_context_keyword ||= begin
34
+ keywords = ::SolidAgent::HasContext::ClassMethods
35
+ .instance_method(:has_context).parameters
36
+ .select { |type, _| [ :key, :keyreq ].include?(type) }
37
+ .map(&:last)
38
+
39
+ keywords.include?(:contextual) ? :contextual : :contextable
40
+ end
41
+ end
23
42
  end
24
43
  end
25
44
 
@@ -26,13 +26,13 @@ class CreateActiveAgentDashboardTables < ActiveRecord::Migration<%= migration_ve
26
26
  t.string :agent_class_name
27
27
  t.string :action_name
28
28
  t.integer :status, default: 0, null: false
29
- t.column :appearance, json_type, default: {}
30
- t.column :instruction_sets, json_type, default: []
31
- t.column :tools, json_type, default: []
32
- t.column :mcp_servers, json_type, default: []
33
- t.column :model_config, json_type, default: {}
34
- t.column :response_format, json_type, default: {}
35
- t.column :action_prompts, json_type, default: [], null: false
29
+ t.column :appearance, json_type, **json_default({})
30
+ t.column :instruction_sets, json_type, **json_default([])
31
+ t.column :tools, json_type, **json_default([])
32
+ t.column :mcp_servers, json_type, **json_default([])
33
+ t.column :model_config, json_type, **json_default({})
34
+ t.column :response_format, json_type, **json_default({})
35
+ t.column :action_prompts, json_type, **json_default([], null: false)
36
36
 
37
37
  # Agents discovered from reported telemetry rather than authored here.
38
38
  t.string :service_name
@@ -52,7 +52,7 @@ class CreateActiveAgentDashboardTables < ActiveRecord::Migration<%= migration_ve
52
52
  create_table "#{prefix}agent_versions" do |t|
53
53
  t.bigint :agent_id, null: false
54
54
  t.integer :version_number, default: 1, null: false
55
- t.column :configuration_snapshot, json_type, default: {}, null: false
55
+ t.column :configuration_snapshot, json_type, **json_default({}, null: false)
56
56
  t.string :change_summary
57
57
  t.string :created_by
58
58
  t.timestamps
@@ -65,10 +65,10 @@ class CreateActiveAgentDashboardTables < ActiveRecord::Migration<%= migration_ve
65
65
  t.string :action_name
66
66
  t.integer :status, default: 0, null: false
67
67
  t.text :input_prompt
68
- t.column :input_params, json_type, default: {}
68
+ t.column :input_params, json_type, **json_default({})
69
69
  t.text :output
70
- t.column :output_metadata, json_type, default: {}
71
- t.column :logs, json_type, default: []
70
+ t.column :output_metadata, json_type, **json_default({})
71
+ t.column :logs, json_type, **json_default([])
72
72
  t.text :error_message
73
73
  t.text :error_backtrace
74
74
  t.integer :input_tokens
@@ -93,11 +93,11 @@ class CreateActiveAgentDashboardTables < ActiveRecord::Migration<%= migration_ve
93
93
  t.string :model, default: "gpt-4o-mini"
94
94
  t.text :instructions
95
95
  t.string :preset_type
96
- t.column :appearance, json_type, default: {}
97
- t.column :instruction_sets, json_type, default: []
98
- t.column :tools, json_type, default: []
99
- t.column :mcp_servers, json_type, default: {}
100
- t.column :model_config, json_type, default: {}
96
+ t.column :appearance, json_type, **json_default({})
97
+ t.column :instruction_sets, json_type, **json_default([])
98
+ t.column :tools, json_type, **json_default([])
99
+ t.column :mcp_servers, json_type, **json_default({})
100
+ t.column :model_config, json_type, **json_default({})
101
101
  t.boolean :featured, default: false
102
102
  t.boolean :free_tier, default: false
103
103
  t.boolean :public, default: true
@@ -114,7 +114,7 @@ class CreateActiveAgentDashboardTables < ActiveRecord::Migration<%= migration_ve
114
114
  t.string :contextable_type
115
115
  t.bigint :contextable_id
116
116
  t.text :instructions
117
- t.column :options, json_type, default: {}
117
+ t.column :options, json_type, **json_default({})
118
118
  t.string :trace_id
119
119
  t.integer :total_input_tokens, default: 0
120
120
  t.integer :total_output_tokens, default: 0
@@ -130,11 +130,11 @@ class CreateActiveAgentDashboardTables < ActiveRecord::Migration<%= migration_ve
130
130
  t.string :content_checksum
131
131
  t.string :tool_call_id
132
132
  t.string :tool_name
133
- t.column :tool_arguments, json_type, default: {}
133
+ t.column :tool_arguments, json_type, **json_default({})
134
134
  t.column :tool_result, json_type
135
- t.column :attachments, json_type, default: []
136
- t.column :metadata, json_type, default: {}
137
- t.column :provenance, json_type, default: {}
135
+ t.column :attachments, json_type, **json_default([])
136
+ t.column :metadata, json_type, **json_default({})
137
+ t.column :provenance, json_type, **json_default({})
138
138
  t.timestamps
139
139
  t.index :agent_context_id
140
140
  end
@@ -150,9 +150,9 @@ class CreateActiveAgentDashboardTables < ActiveRecord::Migration<%= migration_ve
150
150
  t.integer :cached_tokens, default: 0
151
151
  t.integer :reasoning_tokens, default: 0
152
152
  t.float :duration_seconds
153
- t.column :tool_calls, json_type, default: []
153
+ t.column :tool_calls, json_type, **json_default([])
154
154
  t.column :raw_response, json_type
155
- t.column :provenance, json_type, default: {}
155
+ t.column :provenance, json_type, **json_default({})
156
156
  t.string :trace_id
157
157
  t.timestamps
158
158
  t.index :agent_context_id
@@ -183,8 +183,8 @@ class CreateActiveAgentDashboardTables < ActiveRecord::Migration<%= migration_ve
183
183
  t.string :judge_kind, default: "rules", null: false
184
184
  t.string :judge_model
185
185
  t.integer :sample_size, default: 20, null: false
186
- t.column :criteria, json_type, default: [], null: false
187
- t.column :config, json_type, default: {}, null: false
186
+ t.column :criteria, json_type, **json_default([], null: false)
187
+ t.column :config, json_type, **json_default({}, null: false)
188
188
  t.timestamps
189
189
  t.index [ :agent_id, :name ], unique: true
190
190
  end
@@ -192,7 +192,7 @@ class CreateActiveAgentDashboardTables < ActiveRecord::Migration<%= migration_ve
192
192
  create_table "#{prefix}evaluation_runs" do |t|
193
193
  t.bigint :evaluation_id, null: false
194
194
  t.integer :status, default: 0, null: false
195
- t.column :scores, json_type, default: {}
195
+ t.column :scores, json_type, **json_default({})
196
196
  t.integer :samples_evaluated, default: 0
197
197
  t.integer :samples_passed, default: 0
198
198
  t.text :error_message
@@ -208,11 +208,11 @@ class CreateActiveAgentDashboardTables < ActiveRecord::Migration<%= migration_ve
208
208
  t.string :sandbox_type, default: "playwright_mcp"
209
209
  t.string :cloud_run_job_id
210
210
  t.string :cloud_run_url
211
- t.column :runs, json_type, default: []
211
+ t.column :runs, json_type, **json_default([])
212
212
  # MCP servers this session was started with, so the MCP Services view
213
213
  # can show a launched server as running rather than offering to start
214
214
  # a second copy.
215
- t.column :mcp_servers, json_type, default: []
215
+ t.column :mcp_servers, json_type, **json_default([])
216
216
  t.integer :runs_count, default: 0
217
217
  t.integer :max_runs, default: 10
218
218
  t.integer :timeout_seconds, default: 300
@@ -234,7 +234,7 @@ class CreateActiveAgentDashboardTables < ActiveRecord::Migration<%= migration_ve
234
234
  t.integer :status, default: 0, null: false
235
235
  t.text :result
236
236
  t.text :error
237
- t.column :screenshots, json_type, default: []
237
+ t.column :screenshots, json_type, **json_default([])
238
238
  t.integer :tokens_used, default: 0
239
239
  t.integer :duration_ms
240
240
  t.datetime :started_at
@@ -250,7 +250,7 @@ class CreateActiveAgentDashboardTables < ActiveRecord::Migration<%= migration_ve
250
250
  t.integer :status, default: 0, null: false
251
251
  t.integer :action_count, default: 0
252
252
  t.integer :duration_ms
253
- t.column :metadata, json_type, default: {}
253
+ t.column :metadata, json_type, **json_default({})
254
254
  t.bigint :user_id
255
255
  t.bigint :account_id
256
256
  t.timestamps
@@ -269,7 +269,7 @@ class CreateActiveAgentDashboardTables < ActiveRecord::Migration<%= migration_ve
269
269
  t.integer :timestamp_ms, null: false
270
270
  t.string :screenshot_key
271
271
  t.string :dom_snapshot_key
272
- t.column :metadata, json_type, default: {}
272
+ t.column :metadata, json_type, **json_default({})
273
273
  t.timestamps
274
274
  t.index [ :session_recording_id, :sequence ], unique: true,
275
275
  name: "index_active_agent_recording_actions_on_recording_and_sequence"
@@ -314,6 +314,21 @@ class CreateActiveAgentDashboardTables < ActiveRecord::Migration<%= migration_ve
314
314
 
315
315
  # jsonb where the adapter has it, json where it doesn't.
316
316
  def json_type
317
- @json_type ||= connection.adapter_name.to_s.downcase.include?("postgres") ? :jsonb : :json
317
+ @json_type ||= postgres? ? :jsonb : :json
318
+ end
319
+
320
+ # MySQL rejects a default on a JSON column outright ("BLOB, TEXT, GEOMETRY or
321
+ # JSON column can't have a default value"), which aborts create_table, so the
322
+ # column is created without one there. `null: false` is dropped with it:
323
+ # nothing assigns these before validation, so a NOT NULL column with no
324
+ # default would reject the very inserts the default was there to satisfy.
325
+ def json_default(value, null: nil)
326
+ return {} unless postgres?
327
+
328
+ null.nil? ? { default: value } : { default: value, null: null }
329
+ end
330
+
331
+ def postgres?
332
+ connection.adapter_name.to_s.downcase.include?("postgres")
318
333
  end
319
334
  end
@@ -15,9 +15,9 @@ class CreateActiveAgentTelemetryTraces < ActiveRecord::Migration<%= migration_ve
15
15
  t.string :service_name
16
16
  t.string :environment
17
17
  t.datetime :timestamp, index: true
18
- t.column :spans, json_type, default: []
19
- t.column :resource_attributes, json_type, default: {}
20
- t.column :sdk_info, json_type, default: {}
18
+ t.column :spans, json_type, **json_default([])
19
+ t.column :resource_attributes, json_type, **json_default({})
20
+ t.column :sdk_info, json_type, **json_default({})
21
21
  t.decimal :total_duration_ms, precision: 12, scale: 3
22
22
  t.integer :total_input_tokens, default: 0
23
23
  t.integer :total_output_tokens, default: 0
@@ -53,6 +53,19 @@ class CreateActiveAgentTelemetryTraces < ActiveRecord::Migration<%= migration_ve
53
53
  # traverses spans in SQL on PostgreSQL, and jsonb_array_elements rejects a
54
54
  # json argument, so the column type and the query have to agree.
55
55
  def json_type
56
- @json_type ||= connection.adapter_name.to_s.downcase.include?("postgres") ? :jsonb : :json
56
+ @json_type ||= postgres? ? :jsonb : :json
57
+ end
58
+
59
+ # MySQL rejects a default on a JSON column outright ("BLOB, TEXT, GEOMETRY or
60
+ # JSON column can't have a default value"), which aborts create_table, so the
61
+ # column is created without one there. ActionAgent::TelemetryTrace reads every
62
+ # JSON column through `Array(...)`/`|| {}`, and ingest always assigns all
63
+ # three, so a NULL reads the same as the empty default would.
64
+ def json_default(value)
65
+ postgres? ? { default: value } : {}
66
+ end
67
+
68
+ def postgres?
69
+ connection.adapter_name.to_s.downcase.include?("postgres")
57
70
  end
58
71
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: actionagent
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Bowen