actionagent 1.9.7 → 1.9.8

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.
@@ -313,18 +313,37 @@ module ActionAgent
313
313
  LiveUpdates.broadcast("agent_runs_#{agent_id}", type: "update", id: id, status: status)
314
314
  end
315
315
 
316
+ # Statuses a cancel moves to `cancelled`. A finished run keeps its end.
317
+ CANCELLABLE_STATUSES = %i[pending running awaiting_input].freeze
318
+
316
319
  # Cancels a run that is in progress or waiting for input, and every
317
- # request for input it still has pending.
320
+ # request for input it still has pending. Returns whether this call
321
+ # cancelled it.
322
+ #
323
+ # The status is checked by the write itself, as AgentExecutionJob claims
324
+ # a pending run, because this copy of the run may be older than the
325
+ # database's: the request's, or a caller's that has held it since it
326
+ # executed. A run that finished in the meantime is left as it finished,
327
+ # and this copy is reloaded to show how. The update skips callbacks, so
328
+ # the cancel is broadcast here, once.
318
329
  #
319
330
  # @param reason [String] recorded as the run's error message
331
+ # @return [Boolean]
320
332
  def cancel!(reason = "Cancelled by user")
321
- return unless in_progress? || awaiting_input?
333
+ return false unless in_progress? || awaiting_input?
322
334
 
323
- transaction do
324
- update!(status: :cancelled, completed_at: Time.current, error_message: reason)
325
- input_requests.pending.update_all(status: InputRequest.statuses[:cancelled], updated_at: Time.current)
335
+ now = Time.current
336
+ cancelled = transaction do
337
+ changed = self.class.where(id: id, status: self.class.statuses.values_at(*CANCELLABLE_STATUSES))
338
+ .update_all(status: self.class.statuses[:cancelled], completed_at: now, error_message: reason, updated_at: now)
339
+ next false if changed.zero?
340
+
341
+ input_requests.pending.update_all(status: InputRequest.statuses[:cancelled], updated_at: now)
342
+ true
326
343
  end
327
- broadcast_update
344
+ reload
345
+ broadcast_update if cancelled
346
+ cancelled
328
347
  end
329
348
 
330
349
  # Expires the run's overdue requests for input, which fails the run (see
@@ -87,6 +87,48 @@ module ActionAgent
87
87
  comparison if comparison.is_a?(Hash) && comparison.present?
88
88
  end
89
89
 
90
+ # The bars this run was gated by (ActiveAgent::Evals::PassThresholds),
91
+ # as the run recorded them when it started or was imported, so a later
92
+ # change to the catalog does not regrade it; nil for a run whose suite
93
+ # declared none, one recorded before runs kept them, and a framework
94
+ # without per-group bars.
95
+ def pass_thresholds
96
+ self.class.pass_thresholds_from(scores&.dig("_pass_thresholds"))
97
+ end
98
+
99
+ # A recorded `pass_thresholds` mapping (Report#to_h's: "threshold",
100
+ # "min_pass_rate", "groups") as PassThresholds, or nil for anything
101
+ # that is not one.
102
+ def self.pass_thresholds_from(value)
103
+ return nil unless value.is_a?(Hash) && defined?(ActiveAgent::Evals::PassThresholds)
104
+
105
+ value = value.stringify_keys
106
+ groups = value["groups"].nil? ? {} : value["groups"]
107
+ return nil unless groups.is_a?(Hash)
108
+
109
+ ActiveAgent::Evals::PassThresholds.new(suite: value.slice(*ActiveAgent::Evals::PassThresholds::SUITE_KEYS), groups: groups)
110
+ rescue ArgumentError
111
+ nil
112
+ end
113
+
114
+ # The gates the run's results met or missed (Report#gates, each
115
+ # `{ "group", "name", "label", "passed", "total", "pass_rate",
116
+ # "min_pass_rate", "gated", "met" }`), as recorded when it completed;
117
+ # nil for a run without bars.
118
+ def gates
119
+ value = scores&.dig("_gates")
120
+ value.select { |gate| gate.is_a?(Hash) } if value.is_a?(Array)
121
+ end
122
+
123
+ # Whether every gate was met, as Report#passed? answers: false when no
124
+ # gate was reached; nil for a run without bars.
125
+ def gates_met
126
+ rows = gates
127
+ return nil if rows.nil?
128
+
129
+ rows.any? && rows.all? { |gate| gate["met"] == true }
130
+ end
131
+
90
132
  # How the report names the judge, the way the suite panel does: the
91
133
  # judge the recorded verdict names — unless that is only the pass-rate
92
134
  # ranking — else the evaluation's judge model. nil when neither is set,
@@ -307,6 +349,7 @@ module ActionAgent
307
349
  kwargs[:judge_usage] = judge_usage_run if judge_usage_run && self.class.report_accepts?(:judge_usage)
308
350
  kwargs[:release] = release_summary if release_summary && self.class.report_accepts?(:release)
309
351
  kwargs[:comparison] = recorded_comparison if recorded_comparison && self.class.report_accepts?(:comparison)
352
+ kwargs[:pass_thresholds] = pass_thresholds if pass_thresholds && self.class.report_accepts?(:pass_thresholds)
310
353
  self.class.report_class.new(**kwargs)
311
354
  end
312
355
 
@@ -21,6 +21,28 @@ module ActionAgent
21
21
  value.is_a?(Hash) ? value : {}
22
22
  end
23
23
 
24
+ # The product's named criteria lists, as the catalog's `criteria:` gives
25
+ # them: list name => criterion hashes. A stored value that is not such a
26
+ # mapping, and a list left with no criterion, read as no list.
27
+ # @return [Hash{String => Array<Hash>}]
28
+ def criteria
29
+ value = self[:criteria]
30
+ return {} unless value.is_a?(Hash)
31
+
32
+ value.each_with_object({}) do |(name, list), lists|
33
+ entries = Array(list).select { |criterion| criterion.is_a?(Hash) } if list.is_a?(Array)
34
+ lists[name.to_s] = entries if entries.present?
35
+ end
36
+ end
37
+
38
+ # The bars the catalog's `pass:` declares (ActiveAgent::Evals::PassThresholds);
39
+ # a stored value that is not a mapping reads as none.
40
+ # @return [Hash{String => Object}]
41
+ def pass
42
+ value = self[:pass]
43
+ value.is_a?(Hash) ? value : {}
44
+ end
45
+
24
46
  # The agent a set of this product runs against when the caller names
25
47
  # none: the product's agent, else the project's agent under test, else
26
48
  # the owner's agent named by the document.
@@ -36,6 +58,8 @@ module ActionAgent
36
58
  "name" => name,
37
59
  "description" => description,
38
60
  "agent" => agent_name.presence || agent&.name,
61
+ "criteria" => criteria.presence,
62
+ "pass" => pass.presence,
39
63
  "sets" => sets.map(&:to_document)
40
64
  }.merge(metadata).compact
41
65
  end
@@ -37,6 +37,14 @@ module ActionAgent
37
37
  value.is_a?(Hash) ? value : {}
38
38
  end
39
39
 
40
+ # The bars the catalog's `pass:` declares (ActiveAgent::Evals::PassThresholds);
41
+ # a stored value that is not a mapping reads as none.
42
+ # @return [Hash{String => Object}]
43
+ def pass
44
+ value = self[:pass]
45
+ value.is_a?(Hash) ? value : {}
46
+ end
47
+
40
48
  # The evaluation's name: the catalog, product and set keys.
41
49
  def evaluation_name
42
50
  "#{catalog.key}/#{product.key}/#{key}".first(EvaluationReportImport::MAX_EVALUATION_NAME)
@@ -117,6 +125,7 @@ module ActionAgent
117
125
  "description" => description,
118
126
  "judge" => judge.presence,
119
127
  "criteria" => criteria.presence,
128
+ "pass" => pass.presence,
120
129
  "scenarios" => scenarios.map(&:to_document)
121
130
  }.merge(metadata).compact
122
131
  end
@@ -81,7 +81,9 @@ module ActionAgent
81
81
  # checkout sandbox the run replayed against, if any: its session id and
82
82
  # checkout, never its token. `agent_version` is the version of the
83
83
  # agent the run scored, and `version_state` whether that is the agent's
84
- # current version ("current", "earlier" or "unrecorded").
84
+ # current version ("current", "earlier" or "unrecorded"). `gates` are the
85
+ # per-group pass gates the run was held to (EvaluationRun#gates) and
86
+ # `gates_met` whether it met every one; both nil for a run without bars.
85
87
  def run_summary(run, number: nil)
86
88
  {
87
89
  id: run.id,
@@ -95,7 +97,9 @@ module ActionAgent
95
97
  created_at: run.created_at.iso8601,
96
98
  sandbox: run.sandbox,
97
99
  agent_version: run.agent_version_summary,
98
- version_state: run.evaluation.standing_info.version_state(run)
100
+ version_state: run.evaluation.standing_info.version_state(run),
101
+ gates: run.gates,
102
+ gates_met: run.gates_met
99
103
  }
100
104
  end
101
105
 
@@ -95,6 +95,10 @@ module ActionAgent
95
95
  JUDGE_USAGE_KEYS = %w[calls input_tokens output_tokens cost model by_kind source].freeze
96
96
  JUDGE_USAGE_SOURCES = %w[reported traces meter].freeze
97
97
  MAX_JUDGE_KINDS = 10
98
+ # Groups a report's `pass_thresholds` may name; a group holds at least
99
+ # one result, so no report needs more.
100
+ MAX_PASS_GROUPS = MAX_RESULTS
101
+ PASS_THRESHOLD_KEYS = %w[threshold min_pass_rate groups].freeze
98
102
  SCOPE_PATTERN = %r{\A[\w .:/@-]{1,100}\z}
99
103
  STATUSES = %w[passed failed errored].freeze
100
104
  # The envelope statuses a delivery may name.
@@ -674,10 +678,21 @@ module ActionAgent
674
678
  "_verdict" => report["verdict"],
675
679
  "_selection" => selection,
676
680
  "_metadata" => metadata,
677
- "_judge_usage_run" => bounded_judge_usage(report.dig("judge_usage", "run"))
681
+ "_judge_usage_run" => bounded_judge_usage(report.dig("judge_usage", "run")),
682
+ "_pass_thresholds" => recorded_pass_thresholds
678
683
  }.compact.merge("_judge_label" => judge_label)
679
684
  end
680
685
 
686
+ # The bars the report was gated by, as PassThresholds normalizes them
687
+ # (validate_pass_thresholds! has refused anything else), so the rebuilt
688
+ # report gates the stored results the way the application's did. nil
689
+ # for a report whose suite declared none.
690
+ def recorded_pass_thresholds
691
+ return nil if report["pass_thresholds"].nil?
692
+
693
+ EvaluationRun.pass_thresholds_from(report["pass_thresholds"])&.to_h
694
+ end
695
+
681
696
  # The run's scores in the shape the Evaluations view renders
682
697
  # (ScenarioEvaluationRunner#scores_for), summarized from the stored
683
698
  # results as the application reported them — no cost is estimated here,
@@ -689,10 +704,14 @@ module ActionAgent
689
704
  def summarized_scores(run)
690
705
  rebuilt = run.to_report(estimate: false)
691
706
  comparison = rebuilt.comparison if rebuilt.respond_to?(:comparison)
707
+ # The gates are worked out from the stored results as well: the
708
+ # report's own "gates" are not taken as the verdict.
709
+ gates = rebuilt.gates.map(&:to_h) if rebuilt.respond_to?(:pass_thresholds) && rebuilt.pass_thresholds
692
710
  rebuilt.criterion_scores.merge(
693
711
  "_models" => rebuilt.summary_by_model,
694
712
  "_recommendations" => rebuilt.recommendations,
695
- "_comparison" => comparison
713
+ "_comparison" => comparison,
714
+ "_gates" => gates
696
715
  ).compact.merge(recorded_scores)
697
716
  end
698
717
 
@@ -725,6 +744,7 @@ module ActionAgent
725
744
  judge_trace_ids!(metadata["judge_trace_ids"])
726
745
  string!(report["judge"], "report.judge", 200)
727
746
  validate_release!
747
+ validate_pass_thresholds!
728
748
  validate_judge_usage!(report.dig("judge_usage", "run"), "report.judge_usage.run") if report["judge_usage"].is_a?(Hash)
729
749
  optional_object!(report["judge_usage"], "report.judge_usage")
730
750
  object!(report["models"], "report.models")
@@ -856,6 +876,33 @@ module ActionAgent
856
876
  string!(release["label"], "report.release.label", 200)
857
877
  end
858
878
 
879
+ # The bars the suite declared, as Report#to_h writes them: "threshold"
880
+ # and "min_pass_rate" from 0 to 1, and "groups", each group's
881
+ # "min_pass_rate" and "gated", by group key. Checked by PassThresholds
882
+ # itself, so the import refuses what the framework would.
883
+ def validate_pass_thresholds!
884
+ bars = report["pass_thresholds"]
885
+ return if bars.nil?
886
+
887
+ object!(bars, "report.pass_thresholds")
888
+ unknown = bars.keys - PASS_THRESHOLD_KEYS
889
+ raise Invalid, "report.pass_thresholds has unknown keys #{unknown.join(', ')}" if unknown.any?
890
+
891
+ groups = bars["groups"]
892
+ optional_object!(groups, "report.pass_thresholds.groups")
893
+ if groups && groups.size > MAX_PASS_GROUPS
894
+ raise Invalid, "report.pass_thresholds.groups must name at most #{MAX_PASS_GROUPS} groups"
895
+ end
896
+
897
+ (groups || {}).each_key { |key| string!(key, "report.pass_thresholds.groups key", 200) }
898
+ # A framework without per-group bars stores none, as before.
899
+ return unless defined?(ActiveAgent::Evals::PassThresholds)
900
+
901
+ ActiveAgent::Evals::PassThresholds.new(suite: bars.slice(*ActiveAgent::Evals::PassThresholds::SUITE_KEYS), groups: groups || {})
902
+ rescue ArgumentError => e
903
+ raise Invalid, "report.pass_thresholds: #{e.message}"
904
+ end
905
+
859
906
  # What the judge spent, as ActiveAgent::Evals::Report writes it per
860
907
  # result and for the run.
861
908
  def validate_judge_usage!(usage, name)
@@ -181,6 +181,10 @@ module ActionAgent
181
181
  metadata: entry.metadata, agent: resolve_agent(entry) || (merge? ? product.agent : nil),
182
182
  project: resolve_project(entry) || (merge? ? product.project : nil)
183
183
  )
184
+ # Until add_catalog_product_criteria runs, imports keep working without the lists.
185
+ product.criteria = entry.criteria if product.has_attribute?(:criteria)
186
+ # Until add_catalog_pass_bars runs, imports keep working without the bars.
187
+ product.pass = entry.pass if product.has_attribute?(:pass)
184
188
  product.position = position_for(product, entry.position, offset)
185
189
  product.save!
186
190
  replace_sets(product, entry)
@@ -197,6 +201,7 @@ module ActionAgent
197
201
  name: set_entry.name, description: set_entry.description, judge: set_entry.judge,
198
202
  criteria: set_entry.criteria, metadata: set_entry.metadata
199
203
  )
204
+ set.pass = set_entry.pass if set.has_attribute?(:pass)
200
205
  set.position = position_for(set, set_entry.position, offset)
201
206
  set.save!
202
207
  replace_scenarios(set, set_entry)
@@ -22,6 +22,11 @@ module ActionAgent
22
22
  # decisive (ActiveAgent::Evals::Comparison)
23
23
  # "_selection" — the scenarios and models this run covered, and
24
24
  # the checkout sandbox it replayed against, if any
25
+ # "_pass_thresholds" — for an evaluation of a catalog set that declares
26
+ # `pass` bars: the bars, recorded as the run starts,
27
+ # so every replay and the completion gate by the same
28
+ # ones however the catalog changes meanwhile
29
+ # "_gates" — with them: each group's gate per model, met or not
25
30
  #
26
31
  # A selection's `sandbox_id` names a checkout sandbox whose app runtime
27
32
  # every replay reaches beside the agent's own MCP servers, as if the agent
@@ -81,6 +86,7 @@ module ActionAgent
81
86
  specs = model_specs
82
87
  run = @run || @evaluation.evaluation_runs.create!(status: :pending)
83
88
  run.update!(status: :running, selection: selection_summary(scenarios, specs))
89
+ record_pass_thresholds!(run)
84
90
  ensure_sandbox_live!
85
91
 
86
92
  if scenarios.empty?
@@ -125,7 +131,7 @@ module ActionAgent
125
131
  adapter.call(evaluation: @evaluation, owner: owner, scenarios: tasks, models: specs, on_result: metered)
126
132
  else
127
133
  ensure_judge_defined_kpis! if @evaluation.judge_defined?
128
- default_report(tasks, specs, on_result)
134
+ default_report(tasks, specs, on_result, run)
129
135
  end
130
136
  raise ArgumentError, "scenario evaluation adapter must return an ActiveAgent::Evals::Report" unless report.is_a?(Evals::Report)
131
137
  reported = report.results.map { |result| [ result.scenario.key, result.label ] }
@@ -453,7 +459,7 @@ module ActionAgent
453
459
  available_tools: fan_out_state(run).fetch("tools") { tool_roster },
454
460
  instructions: @evaluation.agent.instructions,
455
461
  agent_name: @evaluation.agent.name,
456
- threshold: PASS_THRESHOLD,
462
+ **gating(run.pass_thresholds, Evals::Runner),
457
463
  replay: ->(*) { raise ArgumentError, "a fanned-out result is scored from its stored replay" },
458
464
  refine_gate: -> { reserve_refinement!(run) }
459
465
  )
@@ -513,7 +519,7 @@ module ActionAgent
513
519
  )
514
520
  end
515
521
  report = Evals::Report.new(results: results, models: specs.values, judge: evals_judge,
516
- instructions: @evaluation.agent.instructions, threshold: PASS_THRESHOLD)
522
+ instructions: @evaluation.agent.instructions, **gating(run.pass_thresholds, Evals::Report))
517
523
  state = fan_out_state(run)
518
524
 
519
525
  # The other jobs' judge calls go into this runner's meter before the
@@ -559,7 +565,7 @@ module ActionAgent
559
565
  error.is_a?(Exception) ? "#{error.class}: #{error.message}" : error.to_s
560
566
  end
561
567
 
562
- def default_report(tasks, specs, on_result)
568
+ def default_report(tasks, specs, on_result, run)
563
569
  Evals::Runner.new(
564
570
  scenarios: tasks,
565
571
  models: specs,
@@ -568,7 +574,7 @@ module ActionAgent
568
574
  available_tools: tool_roster,
569
575
  instructions: @evaluation.agent.instructions,
570
576
  agent_name: @evaluation.agent.name,
571
- threshold: PASS_THRESHOLD,
577
+ **gating(run.pass_thresholds, Evals::Runner),
572
578
  replay: ->(scenario, spec) { replay(scenario, spec) },
573
579
  on_result: on_result
574
580
  ).call
@@ -827,6 +833,7 @@ module ActionAgent
827
833
  comparison = report.comparison if report.respond_to?(:comparison)
828
834
  scores["_comparison"] = comparison if comparison
829
835
  scores["_selection"] = run.selection
836
+ record_gates(scores, report, run.pass_thresholds)
830
837
  scores["_metadata"] = report.metadata
831
838
  scores["_judge_label"] = report.judge_label || report.judge&.label
832
839
  # The judge's own spend, apart from the replays' (which the results
@@ -839,6 +846,60 @@ module ActionAgent
839
846
  scores
840
847
  end
841
848
 
849
+ # The bars a host adapter's report did not carry are the run's own, so
850
+ # the run is gated however its replays ran.
851
+ def record_gates(scores, report, recorded)
852
+ thresholds = (report.pass_thresholds if report.respond_to?(:pass_thresholds)) || recorded
853
+ return if thresholds.nil?
854
+
855
+ scores["_pass_thresholds"] = thresholds.to_h
856
+ scores["_gates"] = report.gates(thresholds).map(&:to_h)
857
+ end
858
+
859
+ # --- pass bars ----------------------------------------------------------
860
+
861
+ # The score keyword, and the bars when the run has them and the
862
+ # installed framework's +klass+ takes them: the bars' threshold is the
863
+ # catalog's or PASS_THRESHOLD, the score the dashboard always used.
864
+ def gating(thresholds, klass)
865
+ return { threshold: PASS_THRESHOLD } if thresholds.nil? || !accepts_keyword?(klass, :pass_thresholds)
866
+
867
+ { threshold: thresholds.threshold, pass_thresholds: thresholds }
868
+ end
869
+
870
+ def accepts_keyword?(klass, keyword)
871
+ klass.instance_method(:initialize).parameters.any? { |type, name| name == keyword && %i[key keyreq].include?(type) }
872
+ end
873
+
874
+ # Records the bars of the catalog set this evaluation runs as, once,
875
+ # as the run starts: a fanned-out replay reads them from the run.
876
+ def record_pass_thresholds!(run)
877
+ thresholds = catalog_pass_thresholds
878
+ return if thresholds.nil?
879
+
880
+ run.update!(scores: (run.scores.is_a?(Hash) ? run.scores : {}).merge("_pass_thresholds" => thresholds.to_h))
881
+ end
882
+
883
+ # The product's bars as the suite's and the set's as its group's, as
884
+ # Catalog#suite_for gives them, for an evaluation a catalog set
885
+ # materialized (ScenarioSet#catalog_reference) when the set, under the
886
+ # owner's catalogs, still has the keys the reference names; nil when
887
+ # it declares none, and on a framework without per-group bars.
888
+ def catalog_pass_thresholds
889
+ reference = @evaluation.config.to_h["catalog"]
890
+ return nil unless reference.is_a?(Hash) && defined?(Evals::PassThresholds)
891
+
892
+ set = ScenarioSet.joins(:product).includes(:product)
893
+ .where(ScenarioProduct.table_name => { scenario_catalog_id: ScenarioCatalog.for_owner(owner).select(:id) })
894
+ .find_by(id: reference["set_id"], key: reference["set_key"].to_s)
895
+ return nil if set.nil? || set.product.key != reference["product_key"].to_s
896
+ return nil if set.product.pass.empty? && set.pass.empty?
897
+
898
+ Evals::PassThresholds.new(suite: set.product.pass, groups: { set.key => set.pass })
899
+ rescue ArgumentError
900
+ nil
901
+ end
902
+
842
903
  # Records who judged once the calls are in. A judge with no model pinned
843
904
  # started out labelled by its provider; it is renamed after the model
844
905
  # that answered, here and on the verdict it wrote.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionAgent
4
- VERSION = "1.9.7"
4
+ VERSION = "1.9.8"
5
5
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddCatalogProductCriteria < ActiveRecord::Migration<%= migration_version %>
4
+ def up
5
+ return unless table_exists?(products_table)
6
+ return if column_exists?(products_table, :criteria)
7
+
8
+ type = connection.adapter_name.to_s.downcase.include?("postgres") ? :jsonb : :json
9
+ add_column products_table, :criteria, type
10
+ end
11
+
12
+ def down
13
+ return unless table_exists?(products_table)
14
+ return unless column_exists?(products_table, :criteria)
15
+
16
+ remove_column products_table, :criteria
17
+ end
18
+
19
+ private
20
+
21
+ def products_table
22
+ "#{ActionAgent.table_name_prefix}scenario_products"
23
+ end
24
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddCatalogPassBars < ActiveRecord::Migration<%= migration_version %>
4
+ def up
5
+ type = connection.adapter_name.to_s.downcase.include?("postgres") ? :jsonb : :json
6
+ catalog_tables.each do |table|
7
+ next unless table_exists?(table)
8
+ next if column_exists?(table, :pass)
9
+
10
+ add_column table, :pass, type
11
+ end
12
+ end
13
+
14
+ def down
15
+ catalog_tables.each do |table|
16
+ next unless table_exists?(table)
17
+ next unless column_exists?(table, :pass)
18
+
19
+ remove_column table, :pass
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def catalog_tables
26
+ %w[scenario_products scenario_sets].map { |table| "#{ActionAgent.table_name_prefix}#{table}" }
27
+ end
28
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: actionagent
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.7
4
+ version: 1.9.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Bowen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-11-05 00:00:00.000000000 Z
11
+ date: 2026-11-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activeagent
@@ -383,6 +383,8 @@ files:
383
383
  - lib/generators/action_agent/templates/migrations/030_add_evaluation_scoring_leases.rb.erb
384
384
  - lib/generators/action_agent/templates/migrations/031_add_catalog_scenario_metadata.rb.erb
385
385
  - lib/generators/action_agent/templates/migrations/032_create_active_agent_mcp_server_configs.rb.erb
386
+ - lib/generators/action_agent/templates/migrations/034_add_catalog_product_criteria.rb.erb
387
+ - lib/generators/action_agent/templates/migrations/035_add_catalog_pass_bars.rb.erb
386
388
  - lib/tasks/action_agent.rake
387
389
  - lib/tasks/browser.rake
388
390
  - lib/tasks/claude_code.rake