Class: ActionAgent::Agent

Inherits:
ApplicationRecord show all
Includes:
Ownable
Defined in:
app/models/action_agent/agent.rb

Constant Summary collapse

PRESET_TYPES =

Available presets matching AgentAvatar component

%w[
  terminal webDeveloper documentAnalysis writing translation
  playwright research imageAnalysis computerUse productDesign
].freeze
INSTRUCTION_SETS =

Available instruction sets

%w[
  github ruby rails aws gcp python typescript docker kubernetes
].freeze
AVAILABLE_TOOLS =

Available tools/MCPs

%w[
  terminal playwright filesystem code database slack fetch search edit translate memory agents
].freeze
PROVIDERS =

Available providers

%w[openai anthropic ollama openrouter].freeze
DEFAULT_ACTION =

The default action every agent has; uses the base instructions alone.

"ask"

Constants included from Ownable

Ownable::CLASS_FOR

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Ownable

#owner, #owner=

Methods inherited from ApplicationRecord

for_owner, owner_association

Class Method Details

.polymorphic_nameObject

Polymorphic rows (agent_memories, agent_contexts) store this string. A host app that grew these tables under its own Agent constant keeps its existing rows readable by setting agent_polymorphic_name.



16
17
18
# File 'app/models/action_agent/agent.rb', line 16

def self.polymorphic_name
  ActionAgent.agent_polymorphic_name || super
end

Instance Method Details

#action_prompt_for(action_name) ⇒ Object



103
104
105
# File 'app/models/action_agent/agent.rb', line 103

def action_prompt_for(action_name)
  Array(action_prompts).find { |ap| ap["name"] == action_name.to_s }
end

#available_actionsObject

All invokable action names: the default plus each named action prompt.



99
100
101
# File 'app/models/action_agent/agent.rb', line 99

def available_actions
  [ DEFAULT_ACTION ] + Array(action_prompts).filter_map { |ap| ap["name"].presence }
end

#composed_instructions_for(action_name) ⇒ Object

The system instructions an action executes under: named actions stack their prompt below the agent's base instructions; the default action uses the base instructions alone.



110
111
112
113
# File 'app/models/action_agent/agent.rb', line 110

def composed_instructions_for(action_name)
  action = action_prompt_for(action_name)
  [ instructions, action&.dig("prompt") ].map(&:presence).compact.join("\n\n").presence
end

#configuration_snapshotObject

Returns the configuration as a hash for versioning



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'app/models/action_agent/agent.rb', line 116

def configuration_snapshot
  {
    name: name,
    description: description,
    provider: provider,
    model: model,
    instructions: instructions,
    action_prompts: action_prompts,
    preset_type: preset_type,
    appearance: appearance,
    instruction_sets: instruction_sets,
    tools: tools,
    mcp_servers: mcp_servers,
    model_config: model_config,
    response_format: response_format
  }
end

#execute(input_prompt, action: nil, **params) ⇒ Object

Execute a run with this agent



198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'app/models/action_agent/agent.rb', line 198

def execute(input_prompt, action: nil, **params)
  run = agent_runs.create!(
    input_prompt: input_prompt,
    action_name: normalized_action(action),
    input_params: params,
    status: :pending,
    trace_id: SecureRandom.uuid
  )

  # Queue the execution job
  AgentExecutionJob.perform_later(run.id)

  run
end

#instructions_digest_versionsObject

Maps each historical instructions digest to the first version that introduced it ("v3"), so run cohorts can label instruction changes with real agent versions instead of raw hashes.



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'app/models/action_agent/agent.rb', line 158

def instructions_digest_versions
  agent_versions.order(:version_number).each_with_object({}) do |version, map|
    snapshot = version.configuration_snapshot
    base = snapshot["instructions"]
    label = "v#{version.version_number}"

    if base.present?
      map[Digest::SHA256.hexdigest(base).first(8)] ||= label
    end

    # Named actions run under composed instructions (base + action
    # prompt), so their runs carry a different digest per action.
    Array(snapshot["action_prompts"]).each do |action|
      composed = [ base, action["prompt"] ].map(&:presence).compact.join("\n\n")
      next if composed.blank?

      map[Digest::SHA256.hexdigest(composed).first(8)] ||= label
    end
  end
end

#latest_versionObject

Get the latest version



151
152
153
# File 'app/models/action_agent/agent.rb', line 151

def latest_version
  agent_versions.order(version_number: :desc).first
end

#memoryObject

The agent's long-term memory (solid_agent HasMemory contract) — the summary list its runs read/write via the memory tools.



91
92
93
# File 'app/models/action_agent/agent.rb', line 91

def memory
  AgentMemory.for(self)
end

#restore_from_version!(version) ⇒ Object

Restore from a version



135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'app/models/action_agent/agent.rb', line 135

def restore_from_version!(version)
  config = version.configuration_snapshot
  update!(
    instructions: config["instructions"],
    action_prompts: config["action_prompts"] || [],
    preset_type: config["preset_type"],
    appearance: config["appearance"],
    instruction_sets: config["instruction_sets"],
    tools: config["tools"],
    mcp_servers: config["mcp_servers"],
    model_config: config["model_config"],
    response_format: config["response_format"]
  )
end

#telemetry_agent_classObject

The ActiveAgent class name this agent's runs are recorded under — the correlation key between platform Agent records and telemetry traces (TelemetryTrace#agent_class) and solid_agent contexts.



84
85
86
87
# File 'app/models/action_agent/agent.rb', line 84

def telemetry_agent_class
  base = agent_class_name.presence || name.parameterize(separator: "_").camelize
  base.end_with?("Agent") ? base : "#{base}Agent"
end

#test_execute(input_prompt, action: nil, **params) ⇒ Object

Quick test execution (synchronous)



214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'app/models/action_agent/agent.rb', line 214

def test_execute(input_prompt, action: nil, **params)
  run = agent_runs.create!(
    input_prompt: input_prompt,
    action_name: normalized_action(action),
    input_params: params,
    status: :running,
    trace_id: SecureRandom.uuid,
    started_at: Time.current
  )

  begin
    # Build and execute the agent
    result = AgentExecutionService.call(self, run)

    run.update!(
      output: result[:output],
      output_metadata: result[:metadata],
      status: :complete,
      completed_at: Time.current,
      duration_ms: ((Time.current - run.started_at) * 1000).to_i,
      input_tokens: result.dig(:usage, :input_tokens),
      output_tokens: result.dig(:usage, :output_tokens),
      total_tokens: result.dig(:usage, :total_tokens)
    )
  rescue => e
    run.update!(
      status: :failed,
      completed_at: Time.current,
      error_message: e.message,
      error_backtrace: e.backtrace&.first(10)&.join("\n")
    )
  end

  run
end

#to_agent_class_codeObject

Generate Ruby agent class code



185
186
187
188
189
190
191
192
193
194
195
# File 'app/models/action_agent/agent.rb', line 185

def to_agent_class_code
  <<~RUBY
    class #{agent_class_name || name.camelize}Agent < ApplicationAgent
      generate_with :#{provider}, model: "#{model}"#{model_config_code}

      def perform
        prompt#{instructions_code}
      end
    end
  RUBY
end

#version_countObject

Get version count



180
181
182
# File 'app/models/action_agent/agent.rb', line 180

def version_count
  agent_versions.count
end