Class: ActionAgent::AgentExecutionService
- Inherits:
-
Object
- Object
- ActionAgent::AgentExecutionService
- Defined in:
- app/services/action_agent/agent_execution_service.rb
Overview
Executes a dashboard-configured Agent through the activeagent gem and records a telemetry trace for the run.
The requested provider is used when credentials are available — the account's own provider key (Settings -> Provider API Keys) when configured, else the platform keys in config/active_agent.yml. Without credentials the run fails with an actionable error: execution never falls back to mock output, so every stored run, trace and generation reflects a real provider response. (The gem's mock provider is a test double, accepted only in the test environment.)
Traces are built with the gem's ActiveAgent::Telemetry::Span and persisted through TelemetryTrace.create_from_payload — the same normalizer used by the telemetry ingest endpoint — so platform-executed runs and SDK-reported runs share one pipeline.
Defined Under Namespace
Classes: ProviderNotConfiguredError
Constant Summary collapse
- SERVICE_NAME =
"activeagents-platform"
Class Method Summary collapse
Instance Method Summary collapse
-
#action_name ⇒ Object
The named action this run invokes (falls back to the default).
- #call ⇒ Object
- #composed_instructions ⇒ Object
-
#emit_event(**kwargs) ⇒ Object
Emits a progress event on the run (streamed to the UI by pollers).
-
#event_result_preview(result) ⇒ Object
Compact human preview of a tool result for the live activity feed: prefer the long readable field (page text, sub-agent output) over JSON.
-
#execute_tool(name, **kwargs) ⇒ Object
Routes a provider tool call to its implementation: memory tools bind to the agent record's AgentMemory (the solid_agent HasMemory contract); everything else is stateless and lives in AgentToolbox.
-
#initialize(agent_record, run) ⇒ AgentExecutionService
constructor
A new instance of AgentExecutionService.
- #next_event_id ⇒ Object
-
#provider ⇒ Object
Returns the provider used for this execution, or raises when its credentials are missing.
-
#record_prompt_span(root_span) ⇒ Object
The outbound prompt as a span, in the SDK's attribute shape — gives the Traces UI its System/User conversation rows and lets the context-pressure meter attribute instructions and tool schemas instead of lumping the whole input into "messages".
- #requested_model ⇒ Object
- #requested_provider ⇒ Object
-
#run_params ⇒ Object
Per-run provider/model overrides (input_params) let callers replay the same agent under a different model — the basis of evaluation comparison runs.
Constructor Details
#initialize(agent_record, run) ⇒ AgentExecutionService
Returns a new instance of AgentExecutionService.
30 31 32 33 34 35 |
# File 'app/services/action_agent/agent_execution_service.rb', line 30 def initialize(agent_record, run) @agent_record = agent_record @run = run @tool_invocations = [] @event_sequence = 0 end |
Class Method Details
.call(agent_record, run) ⇒ Object
26 27 28 |
# File 'app/services/action_agent/agent_execution_service.rb', line 26 def self.call(agent_record, run) new(agent_record, run).call end |
Instance Method Details
#action_name ⇒ Object
The named action this run invokes (falls back to the default). Named actions execute under composed instructions: base + the action's prompt.
176 177 178 179 180 181 |
# File 'app/services/action_agent/agent_execution_service.rb', line 176 def action_name @action_name ||= begin requested = @run.action_name.presence || Agent::DEFAULT_ACTION @agent_record.available_actions.include?(requested) ? requested : Agent::DEFAULT_ACTION end end |
#call ⇒ Object
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'app/services/action_agent/agent_execution_service.rb', line 61 def call root_span = @root_span = build_root_span record_prompt_span(root_span) llm_span = root_span.add_span( "llm.generate", span_type: :llm, "llm.provider" => provider.to_s, "llm.model" => model ) llm_eid = next_event_id llm_started = Process.clock_gettime(Process::CLOCK_MONOTONIC) emit_event(eid: llm_eid, kind: "llm", label: "#{provider}/#{model} generating", status: "started") begin response = generate! usage = response.usage input = usage&.input_tokens.to_i output = usage&.output_tokens.to_i thinking = usage&.reasoning_tokens.to_i llm_span.set_tokens(input: input, output: output, thinking: thinking) llm_span.finish emit_event( eid: llm_eid, kind: "llm", label: "#{provider}/#{model} generating", status: "done", duration_ms: ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - llm_started) * 1000).round, detail: "#{input} in / #{output} out tokens#{thinking.positive? ? " / #{thinking} thinking" : ""}" ) tool_calls = record_tool_spans(root_span, response) (response) sync_context_instructions root_span.finish { output: response.&.content, metadata: { provider: provider.to_s, model: model, action: action_name, instructions: composed_instructions, requested_provider: @agent_record.provider, trace_id: root_span.trace_id, context_id: conversation_context&.id, tool_calls: tool_calls }, usage: { input_tokens: input, output_tokens: output, total_tokens: usage&.total_tokens || input + output + thinking } } rescue StandardError => e llm_span.record_error(e) llm_span.finish root_span.record_error(e) root_span.finish emit_event(eid: llm_eid, kind: "llm", label: "#{provider}/#{model} generating", status: "error", detail: e.) raise ensure record_trace(root_span) end end |
#composed_instructions ⇒ Object
183 184 185 |
# File 'app/services/action_agent/agent_execution_service.rb', line 183 def composed_instructions @composed_instructions ||= @agent_record.composed_instructions_for(action_name) end |
#emit_event(**kwargs) ⇒ Object
Emits a progress event on the run (streamed to the UI by pollers). Never lets telemetry break execution.
39 40 41 42 43 |
# File 'app/services/action_agent/agent_execution_service.rb', line 39 def emit_event(**kwargs) @run.append_event(**kwargs) rescue StandardError => e Rails.logger.warn("[AgentExecutionService] event emit failed: #{e.}") end |
#event_result_preview(result) ⇒ Object
Compact human preview of a tool result for the live activity feed: prefer the long readable field (page text, sub-agent output) over JSON.
52 53 54 55 56 57 58 59 |
# File 'app/services/action_agent/agent_execution_service.rb', line 52 def event_result_preview(result) return nil unless result.respond_to?(:[]) readable = %i[text output content body].filter_map { |field| result[field] || result[field.to_s] } .find { |value| value.is_a?(String) && value.strip.present? } preview = readable ? readable.gsub(/\s+/, " ").strip : result.to_json preview.byteslice(0, 1000).to_s.scrub end |
#execute_tool(name, **kwargs) ⇒ Object
Routes a provider tool call to its implementation: memory tools bind to the agent record's AgentMemory (the solid_agent HasMemory contract); everything else is stateless and lives in AgentToolbox.
Each call is wrapped in a live :tool span (real start/end around the execution) and recorded in @tool_invocations so tool names, arguments and durations reach Traces and the persisted conversation.
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 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 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 |
# File 'app/services/action_agent/agent_execution_service.rb', line 194 def execute_tool(name, **kwargs) # Record the absolute URL browse_page will actually fetch, not the bare # path the model passed — spans/events/persisted args stay unambiguous. kwargs[:url] = AgentToolbox.resolve_browse_url(kwargs[:url]) if name.to_s == "browse_page" && kwargs[:url] span = @root_span&.add_span("tool.#{name}", span_type: :tool) span&.set_attribute("tool.name", name.to_s) # tool.input.args is the key the Traces UI and TraceInteractionSerializer # read — the call's in: side. span&.set_attribute("tool.input.args", kwargs.to_json.byteslice(0, 500).to_s.scrub) if kwargs.present? started = Process.clock_gettime(Process::CLOCK_MONOTONIC) event_kind = name.to_s == "call_agent" ? "agent" : "tool" event_label = name.to_s == "call_agent" ? "call_agent → #{kwargs[:slug]}" : name.to_s event_id = next_event_id emit_event(eid: event_id, kind: event_kind, label: event_label, status: "started", detail: kwargs.to_json) result = begin case name.to_s when "save_memory" entry = agent_memory.remember( kwargs[:content].to_s, source_agent: agent_class_name, category: kwargs[:category] ) { saved: true, id: entry.id, content: entry.content } when "recall_memory" entries = agent_memory.recall(limit: kwargs[:limit], category: kwargs[:category]) { count: entries.size, entries: entries.map do |entry| { content: entry.content, category: entry.category, source_agent: entry.source_agent, created_at: entry.created_at&.iso8601 }.compact end } when "call_agent" call_agent(slug: kwargs[:slug], message: kwargs[:message]) else AgentToolbox.call(name, **kwargs) end rescue StandardError => e Rails.logger.warn("[AgentExecutionService] Tool #{name} failed: #{e.class} - #{e.}") { error: "#{name} failed: #{e.}" } end duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round(2) errored = result.respond_to?(:key?) && (result.key?(:error) || result.key?("error")) span&.set_attribute("tool.error", true) if errored # Record the readable side of the result (most tools wrap one long text # field); byteslicing whole-JSON breaks it mid-string and the UI can't # parse the remainder. result_text = if result.respond_to?(:key?) && (result[:text] || result["text"]).is_a?(String) result[:text] || result["text"] else result.to_json end span&.set_attribute("tool.output.result", result_text.byteslice(0, 4000).to_s.scrub) span&.finish emit_event( eid: event_id, kind: event_kind, label: event_label, status: errored ? "error" : "done", duration_ms: duration_ms, detail: errored ? (result[:error] || result["error"]).to_s : event_result_preview(result) ) @tool_invocations << { name: name.to_s, arguments: kwargs, duration_ms: duration_ms, error: errored } result end |
#next_event_id ⇒ Object
45 46 47 48 |
# File 'app/services/action_agent/agent_execution_service.rb', line 45 def next_event_id @event_sequence += 1 "#{@run.id}-#{@event_sequence}" end |
#provider ⇒ Object
Returns the provider used for this execution, or raises when its credentials are missing.
163 164 165 166 167 168 169 170 171 172 |
# File 'app/services/action_agent/agent_execution_service.rb', line 163 def provider @provider ||= begin unless provider_available?(requested_provider) raise ProviderNotConfiguredError, "No credentials configured for provider '#{requested_provider}' — " \ "add an API key in Settings -> Provider API Keys, or configure platform credentials" end requested_provider.to_sym end end |
#record_prompt_span(root_span) ⇒ Object
The outbound prompt as a span, in the SDK's attribute shape — gives the Traces UI its System/User conversation rows and lets the context-pressure meter attribute instructions and tool schemas instead of lumping the whole input into "messages".
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'app/services/action_agent/agent_execution_service.rb', line 128 def record_prompt_span(root_span) span = root_span.add_span("agent.prompt", span_type: :prompt) if composed_instructions.present? span.set_attribute("prompt.input.instructions", composed_instructions.to_s.byteslice(0, 6000).to_s.scrub) end if tool_schemas.present? span.set_attribute("prompt.input.tools", tool_schemas.to_json.byteslice(0, 6000).to_s.scrub) end span.set_attribute( "prompt.input.messages", [ { role: "user", content: @run.input_prompt.to_s.byteslice(0, 4000).to_s.scrub } ].to_json ) span.set_attribute("messages.count", 1) span.finish rescue StandardError => e Rails.logger.warn("[AgentExecutionService] prompt span failed: #{e.}") end |
#requested_model ⇒ Object
157 158 159 |
# File 'app/services/action_agent/agent_execution_service.rb', line 157 def requested_model @requested_model ||= run_params[:model_override].presence || @agent_record.model end |
#requested_provider ⇒ Object
153 154 155 |
# File 'app/services/action_agent/agent_execution_service.rb', line 153 def requested_provider @requested_provider ||= (run_params[:provider_override].presence || @agent_record.provider).to_s end |
#run_params ⇒ Object
Per-run provider/model overrides (input_params) let callers replay the same agent under a different model — the basis of evaluation comparison runs. Absent overrides, the agent's own configuration applies.
149 150 151 |
# File 'app/services/action_agent/agent_execution_service.rb', line 149 def run_params @run_params ||= (@run.input_params || {}).with_indifferent_access end |