Class: ActionAgent::AgentExecutions

Inherits:
Object
  • Object
show all
Defined in:
app/queries/action_agent/agent_executions.rb

Overview

One agent execution, whoever ran it.

The platform records executions it initiates as AgentRun rows; agents running inside a customer's own app only ever produce a TelemetryTrace. They are the same grain — one attempt at one agent action — so the dashboard treats them as one list with a source discriminator rather than as two competing tabs:

dashboard  an AgentRun (we executed it; has lifecycle, logs, cancel)
reported   a trace no AgentRun claims (their app executed it)

A platform execution usually writes BOTH an AgentRun and a trace sharing a trace_id, so traces are counted only when unclaimed. The reverse is not symmetric: a run that fails before its root span exists has no trace at all, which is why runs — not traces — are the authoritative source when both exist.

Distinct from AgentContext, which is a stream: one durable conversation per agent action that many executions append to. A stream is a parent of executions, not an alternative to them.

Defined Under Namespace

Classes: Row

Constant Summary collapse

SOURCES =
%w[dashboard reported].freeze
SORTS =

What to rank a list of executions by. "Popular" is deliberately absent: it's an aggregate property of an agent or a model, not of one attempt — the agent list and the metrics table sort by it, this list can't.

{
  "recent" => "Most recent",
  "longest" => "Longest running",
  "cost" => "Highest cost",
  "tokens" => "Most tokens"
}.freeze
DEFAULT_SORT =
"recent"

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(agents:, owner: nil, window_minutes: nil, source: nil, status: nil, sort: nil) ⇒ AgentExecutions

Returns a new instance of AgentExecutions.

Parameters:

  • agents (ActiveRecord::Relation, Array<Agent>)

    scope to these agents

  • owner (Object, nil) (defaults to: nil)

    tenant whose reported traces are in scope; nil in a single-tenant install, where every trace is in scope

  • window_minutes (Integer, nil) (defaults to: nil)

    nil means all time

  • source (String, nil) (defaults to: nil)

    "dashboard" | "reported" | nil for both

  • status (String, nil) (defaults to: nil)

    AgentRun status; also filters reported by OK/ERROR

  • sort (String, nil) (defaults to: nil)

    one of SORTS; unknown values fall back to recent



62
63
64
65
66
67
68
69
# File 'app/queries/action_agent/agent_executions.rb', line 62

def initialize(agents:, owner: nil, window_minutes: nil, source: nil, status: nil, sort: nil)
  @agent_ids = Array(agents.respond_to?(:pluck) ? agents.pluck(:id) : agents.map(&:id))
  @owner = owner
  @window_minutes = window_minutes
  @source = source.presence
  @status = status.presence
  @sort = SORTS.key?(sort.to_s) ? sort.to_s : DEFAULT_SORT
end

Class Method Details

.unclaimed_traces(agent_ids, since: nil, owner: nil) ⇒ Object

Traces attributed to these agents that no AgentRun already accounts for. Shared with AgentScorecard so every surface counts the same executions.



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'app/queries/action_agent/agent_executions.rb', line 93

def self.unclaimed_traces(agent_ids, since: nil, owner: nil)
  traces = ActionAgent.trace_model
  runs_table = AgentRun.table_name
  traces_table = traces.table_name

  scope = traces.where(agent_id: agent_ids)
  scope = scope.(owner) if owner
  scope = scope.where(timestamp: since..) if since
  scope
    .joins(<<~SQL.squish)
      LEFT JOIN #{runs_table}
        ON #{runs_table}.trace_id = #{traces_table}.trace_id
       AND #{runs_table}.agent_id = #{traces_table}.agent_id
    SQL
    .where(runs_table => { id: nil })
end

Instance Method Details

#page(page: 1, per_page: 20) ⇒ Object

Ranked by the chosen dimension, descending. The two sources can only be ordered together in Ruby (no SQL union), which is also why this always ranks the whole window rather than the page.



74
75
76
77
78
# File 'app/queries/action_agent/agent_executions.rb', line 74

def page(page: 1, per_page: 20)
  all = sorted_rows
  offset = (page.to_i - 1) * per_page.to_i
  { rows: all[offset, per_page.to_i] || [], total: all.size }
end

#rowsObject



87
88
89
# File 'app/queries/action_agent/agent_executions.rb', line 87

def rows
  @rows ||= (include_dashboard? ? run_rows : []) + (include_reported? ? trace_rows : [])
end

#sorted_rowsObject



80
81
82
83
84
85
# File 'app/queries/action_agent/agent_executions.rb', line 80

def sorted_rows
  # Ties (and rows missing the sorted value entirely — an execution still
  # running has no duration) fall back to newest first, so the list never
  # reorders arbitrarily between requests.
  rows.sort_by { |row| [ -sort_value(row), -(row.occurred_at || Time.at(0)).to_f ] }
end