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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ Releases `activeagent` and `actionagent` 1.6.3 from one tag.
rather than failing. The keyword reaches only a block that names it or
collects `**`, so judges taking `instructions:` and `prompt:` are unaffected.
(#462)
- `Agent#generations` reads the generations recorded against an agent, with
`Agent#agent_contexts` beside it. Generations hang off `AgentContext`
polymorphically, so reaching them meant hand-writing that join — the engine
did it itself in a private service method a host could not reuse, which now
uses the association instead. Destroying an agent still leaves its contexts
alone, as it always has. (#464)

### Fixed

Expand Down
8 changes: 8 additions & 0 deletions actionagent/app/models/action_agent/agent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ class ObservedAgentError < StandardError; end
has_many :agent_runs, dependent: :destroy
has_many :evaluations, dependent: :destroy
has_many :agent_memories, as: :memorable, dependent: :destroy
# Generations hang off AgentContext polymorphically, which is an
# implementation detail of how contexts are modelled — so without these a
# host that wants an agent's recorded history writes that join itself and is
# coupled to the shape. Deliberately no `dependent:` on the contexts: the
# association is added to read them, and destroying an agent has never taken
# its conversations with it. Making it do so is a separate call.
has_many :agent_contexts, as: :contextable
has_many :generations, through: :agent_contexts

# Polymorphic rows (agent_memories, agent_contexts) store this string.
# A host app that grew these tables under its own Agent constant keeps
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,7 @@ def passed_count(per_sample_scores)
end

def sample_generations(model: nil)
scope = AgentGeneration
.joins(:agent_context)
.where(AgentContext.table_name => { contextable: @evaluation.agent })
scope = @evaluation.agent.generations
scope = scope.where(model: model) if model
scope.order(created_at: :desc).limit(@evaluation.sample_size).to_a
end
Expand Down
49 changes: 49 additions & 0 deletions actionagent/test/agent_generations_association_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

require "test_helper"

# Generations hang off AgentContext polymorphically, so reading an agent's
# recorded history meant writing that join by hand — the engine's own
# EvaluationRunnerService did, in a private method a host cannot reuse. The
# association makes it one definition and keeps the shape behind the engine
# boundary.
class ActionAgentAgentGenerationsAssociationTest < ActiveSupport::TestCase
def setup
@agent = ActionAgent::Agent.create!(name: "Support", provider: "mock", model: "mock")
@other = ActionAgent::Agent.create!(name: "Other", provider: "mock", model: "mock")
end

def generation_for(agent, model:, content: "hi")
context = ActionAgent::AgentContext.create!(contextable: agent, agent_name: "SupportAgent", action_name: "respond")
ActionAgent::AgentGeneration.create!(agent_context: context, model: model, content: content)
end

test "an agent reads the generations recorded against it" do
mine = generation_for(@agent, model: "gpt-5.5")
theirs = generation_for(@other, model: "gpt-5.5")

assert_equal [ mine.id ], @agent.generations.pluck(:id)
assert_equal [ theirs.id ], @other.generations.pluck(:id)
end

test "generations is a scope, so it counts and filters without loading" do
generation_for(@agent, model: "gpt-5.5")
generation_for(@agent, model: "claude-opus-5")

assert_equal 2, @agent.generations.count
assert_equal 1, @agent.generations.where(model: "gpt-5.5").count
# The dashboard's "has this agent actually run?" question.
assert_equal 0, @other.generations.count
end

# The association exists to read generations. Destroying an agent has never
# taken its contexts with it, and this must not quietly start doing so.
test "destroying an agent leaves its contexts alone, as before" do
generation_for(@agent, model: "gpt-5.5")
context_count = ActionAgent::AgentContext.count

@agent.destroy!

assert_equal context_count, ActionAgent::AgentContext.count
end
end
Loading