actionagent 1.2.1 → 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: 9d2d4c834fd9d4b7990cd9deb27061d5b56e25e1379ae66c1b7251c0c890b645
4
- data.tar.gz: 84903be1227333260c6b7b90079c4a67f1d5fc60c07bafc6b28651b81b8804cf
3
+ metadata.gz: f6e31cd17a84e6621881630137c3478268d2e31c8bba138516a397c23964c4f9
4
+ data.tar.gz: 28e83e7b84704c5ff9de2b475998273ac3dd328929515472f31e3743981927de
5
5
  SHA512:
6
- metadata.gz: d11a547b4172f715efa0a16c3da0f548e06ff17ac2b7c7524b9a696d16a4f1248123ca9d2f859187862112b1b3fbfdafd73a556891eaf4bcd810492eb92be81f
7
- data.tar.gz: bda14fd2e530157019b6b823581d08c42eb9896a4db726f1325b03b339e14f21b0e97cb003bc693e131440ed6a56d5ffca7f330ba147ddcf8d89a77994fedc85
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,26 +33,34 @@ module ActionAgent
26
33
  app.config.filter_parameters += [ :credential, :api_key, :access_token ]
27
34
  end
28
35
 
29
- # The controllers under app/controllers/action_agent/api are ActionAgent::Api,
30
- # but an engine's files are autoloaded by the host's `rails.main` loader,
31
- # under the host's inflections. A host that declares `inflect.acronym "API"`
32
- # common enough that Rails documents it makes Zeitwerk expect
33
- # ActionAgent::API::TracesController in a file that defines
34
- # ActionAgent::Api::TracesController, and every request to the mount raises
35
- # Zeitwerk::NameError.
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.
36
52
  #
37
- # Scoped to this engine's own path rather than set through `inflect`: the
38
- # loader is shared, so a blanket rule would re-spell the host's own API
39
- # constants. `camelize` receives the absolute path, which is the only hook
40
- # that can tell this engine's api/ from the host's.
53
+ # Basenames whose spelling this engine cannot express through default
54
+ # camelization (a genuine acronym it wants uppercased) go in OVERRIDES.
41
55
  initializer "action_agent.inflections", before: :set_autoload_paths do
42
- engine_root = root.to_s
56
+ engine_root = File.join(root.to_s, "")
57
+ default = Zeitwerk::Inflector.new
43
58
 
44
59
  Rails.autoloaders.main.inflector.singleton_class.prepend(Module.new do
45
60
  define_method(:camelize) do |basename, abspath|
46
- next "Api" if basename == "api" && abspath.to_s.start_with?(engine_root)
61
+ next super(basename, abspath) unless abspath.to_s.start_with?(engine_root)
47
62
 
48
- super(basename, abspath)
63
+ ActionAgent::Engine::INFLECTION_OVERRIDES.fetch(basename) { default.camelize(basename, abspath) }
49
64
  end
50
65
  end)
51
66
  end
@@ -59,13 +74,33 @@ module ActionAgent
59
74
  # So the namespace answers to both. `const_missing` rather than an eager
60
75
  # alias because the controllers are autoloaded on demand, and naming them at
61
76
  # boot would load the whole dashboard.
62
- ActionAgent.singleton_class.prepend(Module.new do
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
63
85
  def const_missing(name)
64
- return const_get(:Api) if name == :API
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
65
93
 
66
- super
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)
67
102
  end
68
- end)
103
+ end
69
104
 
70
105
  initializer "action_agent.assets", before: :append_assets_path do |app|
71
106
  builds = root.join("app", "assets", "builds").to_s
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionAgent
4
- VERSION = "1.2.1"
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
 
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.1
4
+ version: 1.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Bowen