Class: ActionAgent::AgentRun

Inherits:
ApplicationRecord show all
Defined in:
app/models/action_agent/agent_run.rb

Constant Summary collapse

CODENAME_ADJECTIVES =

Deterministic memorable name for the digest ("calm-heron") — reads far better than hex when comparing cohorts, and is stable across runs and deployments because it's derived from the digest alone.

%w[
  calm brisk quiet bold amber coral dusky fresh golden keen
  lively mellow nimble pale rustic silver tidal vivid wry zesty
  arid breezy crisp dapper eager foggy hazy icy jolly lunar
  misty polar
].freeze
CODENAME_NOUNS =
%w[
  heron otter falcon cedar willow harbor mesa ridge grove delta
  prairie summit canyon reef atoll fjord tundra oasis lagoon dune
  glacier meadow bluff cove marsh basin knoll strait quarry vale
  hollow crag
].freeze

Instance Method Summary collapse

Methods inherited from ApplicationRecord

for_owner, owner_association

Instance Method Details

#add_log(message, level: :info) ⇒ Object

Add a log entry



24
25
26
27
28
29
30
31
32
# File 'app/models/action_agent/agent_run.rb', line 24

def add_log(message, level: :info)
  new_logs = logs || []
  new_logs << {
    timestamp: Time.current.iso8601,
    level: level.to_s,
    message: message
  }
  update!(logs: new_logs)
end

#append_event(eid:, kind:, label:, status: "done", detail: nil, duration_ms: nil) ⇒ Object

Appends a progress event to logs mid-run so pollers can stream what the agent is doing (pending llm/tool/agent calls). Events pair up by eid: a "started" event is pending until a "done"/"error" with the same eid lands. update_column: no validations/callbacks, safe from the run's own execution thread; reads current DB state so add_log interleaves safely.



39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'app/models/action_agent/agent_run.rb', line 39

def append_event(eid:, kind:, label:, status: "done", detail: nil, duration_ms: nil)
  event = {
    "at" => Time.current.iso8601(3),
    "eid" => eid,
    "kind" => kind.to_s,
    "label" => label.to_s,
    "status" => status.to_s
  }
  event["detail"] = detail.to_s.byteslice(0, 1200).to_s.scrub if detail
  event["duration_ms"] = duration_ms if duration_ms
  current = self.class.where(id: id).pick(:logs) || []
  update_column(:logs, current + [ event ])
  event
end

#broadcast_updateObject

Stream output updates via ActionCable



127
128
129
130
131
# File 'app/models/action_agent/agent_run.rb', line 127

def broadcast_update
  payload = { type: "update", run: summary }
  ActionCable.server.broadcast("agent_run_#{id}", payload)
  ActionCable.server.broadcast("agent_runs_#{agent_id}", payload)
end

#calculated_duration_msObject

Calculate duration if not set



89
90
91
92
93
94
# File 'app/models/action_agent/agent_run.rb', line 89

def calculated_duration_ms
  return duration_ms if duration_ms.present?
  return nil unless started_at && completed_at

  ((completed_at - started_at) * 1000).to_i
end

#cancel!Object

Cancel a running execution



134
135
136
137
138
139
140
141
142
143
# File 'app/models/action_agent/agent_run.rb', line 134

def cancel!
  return unless in_progress?

  update!(
    status: :cancelled,
    completed_at: Time.current,
    error_message: "Cancelled by user"
  )
  broadcast_update
end

#finished?Boolean

Check if run is finished

Returns:

  • (Boolean)


102
103
104
# File 'app/models/action_agent/agent_run.rb', line 102

def finished?
  complete? || failed? || cancelled?
end

#in_progress?Boolean

Check if run is still in progress

Returns:

  • (Boolean)


97
98
99
# File 'app/models/action_agent/agent_run.rb', line 97

def in_progress?
  pending? || running?
end

#instructions_codenameObject



80
81
82
83
84
85
86
# File 'app/models/action_agent/agent_run.rb', line 80

def instructions_codename
  digest = instructions_digest
  return nil unless digest

  value = digest.to_i(16)
  "#{CODENAME_ADJECTIVES[value % 32]}-#{CODENAME_NOUNS[(value / 32) % 32]}"
end

#instructions_digestObject

Stable short fingerprint of the instructions this run executed under — the grouping key (with model) for configuration cohorts when comparing instruction/model changes.



57
58
59
60
61
62
# File 'app/models/action_agent/agent_run.rb', line 57

def instructions_digest
  instructions = &.dig("instructions")
  return nil if instructions.blank?

  Digest::SHA256.hexdigest(instructions).first(8)
end

#summaryObject

Get a summary for display



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'app/models/action_agent/agent_run.rb', line 107

def summary
  {
    id: id,
    status: status,
    input_preview: input_prompt&.truncate(100),
    output_preview: output&.truncate(200),
    duration_ms: calculated_duration_ms,
    tokens: total_tokens,
    provider: &.dig("provider"),
    model: &.dig("model"),
    action_name: action_name || &.dig("action") || "ask",
    instructions_digest: instructions_digest,
    instructions_codename: instructions_codename,
    instructions_preview: &.dig("instructions")&.truncate(120),
    created_at: created_at,
    error: error_message
  }
end