ruby_llm-agents 3.5.4 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -0
  3. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +155 -10
  4. data/app/helpers/ruby_llm/agents/application_helper.rb +15 -1
  5. data/app/models/ruby_llm/agents/execution/replayable.rb +124 -0
  6. data/app/models/ruby_llm/agents/execution/scopes.rb +42 -1
  7. data/app/models/ruby_llm/agents/execution.rb +50 -1
  8. data/app/models/ruby_llm/agents/tenant/budgetable.rb +28 -4
  9. data/app/views/layouts/ruby_llm/agents/application.html.erb +41 -28
  10. data/app/views/ruby_llm/agents/agents/show.html.erb +16 -1
  11. data/app/views/ruby_llm/agents/dashboard/_top_tenants.html.erb +47 -0
  12. data/app/views/ruby_llm/agents/dashboard/index.html.erb +397 -100
  13. data/lib/generators/ruby_llm_agents/rename_agent_generator.rb +53 -0
  14. data/lib/generators/ruby_llm_agents/templates/rename_agent_migration.rb.tt +19 -0
  15. data/lib/ruby_llm/agents/agent_tool.rb +125 -0
  16. data/lib/ruby_llm/agents/audio/speaker.rb +5 -3
  17. data/lib/ruby_llm/agents/audio/speech_pricing.rb +63 -187
  18. data/lib/ruby_llm/agents/audio/transcriber.rb +5 -3
  19. data/lib/ruby_llm/agents/audio/transcription_pricing.rb +5 -7
  20. data/lib/ruby_llm/agents/base_agent.rb +144 -5
  21. data/lib/ruby_llm/agents/core/configuration.rb +178 -53
  22. data/lib/ruby_llm/agents/core/errors.rb +3 -77
  23. data/lib/ruby_llm/agents/core/instrumentation.rb +0 -17
  24. data/lib/ruby_llm/agents/core/version.rb +1 -1
  25. data/lib/ruby_llm/agents/dsl/base.rb +0 -8
  26. data/lib/ruby_llm/agents/dsl/queryable.rb +124 -0
  27. data/lib/ruby_llm/agents/dsl.rb +1 -0
  28. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +2 -1
  29. data/lib/ruby_llm/agents/image/generator/pricing.rb +75 -217
  30. data/lib/ruby_llm/agents/image/generator.rb +5 -3
  31. data/lib/ruby_llm/agents/infrastructure/attempt_tracker.rb +8 -0
  32. data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +4 -2
  33. data/lib/ruby_llm/agents/pipeline/builder.rb +43 -0
  34. data/lib/ruby_llm/agents/pipeline/context.rb +11 -1
  35. data/lib/ruby_llm/agents/pipeline/executor.rb +1 -25
  36. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +26 -1
  37. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +18 -0
  38. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +130 -3
  39. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +29 -0
  40. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +11 -4
  41. data/lib/ruby_llm/agents/pipeline.rb +0 -92
  42. data/lib/ruby_llm/agents/results/background_removal_result.rb +11 -1
  43. data/lib/ruby_llm/agents/results/base.rb +23 -1
  44. data/lib/ruby_llm/agents/results/embedding_result.rb +14 -1
  45. data/lib/ruby_llm/agents/results/image_analysis_result.rb +11 -1
  46. data/lib/ruby_llm/agents/results/image_edit_result.rb +11 -1
  47. data/lib/ruby_llm/agents/results/image_generation_result.rb +12 -3
  48. data/lib/ruby_llm/agents/results/image_pipeline_result.rb +11 -1
  49. data/lib/ruby_llm/agents/results/image_transform_result.rb +11 -1
  50. data/lib/ruby_llm/agents/results/image_upscale_result.rb +11 -1
  51. data/lib/ruby_llm/agents/results/image_variation_result.rb +11 -1
  52. data/lib/ruby_llm/agents/results/speech_result.rb +20 -1
  53. data/lib/ruby_llm/agents/results/transcription_result.rb +20 -1
  54. data/lib/ruby_llm/agents/text/embedder.rb +23 -18
  55. data/lib/ruby_llm/agents.rb +70 -5
  56. data/lib/tasks/ruby_llm_agents.rake +21 -0
  57. metadata +7 -6
  58. data/lib/ruby_llm/agents/infrastructure/reliability/breaker_manager.rb +0 -80
  59. data/lib/ruby_llm/agents/infrastructure/reliability/execution_constraints.rb +0 -69
  60. data/lib/ruby_llm/agents/infrastructure/reliability/executor.rb +0 -125
  61. data/lib/ruby_llm/agents/infrastructure/reliability/fallback_routing.rb +0 -72
  62. data/lib/ruby_llm/agents/infrastructure/reliability/retry_strategy.rb +0 -82
@@ -51,36 +51,12 @@ module RubyLLM
51
51
  # @param context [Context] The execution context
52
52
  # @return [Context] The context with output set
53
53
  def call(context)
54
- @agent.execute(context)
54
+ @agent.send(:execute, context)
55
55
  context
56
56
  end
57
57
  end
58
58
 
59
59
  # Lambda-based executor for simple cases
60
- #
61
- # Allows wrapping a lambda/proc as the core executor,
62
- # useful for testing or simple agents.
63
- #
64
- # @example
65
- # executor = LambdaExecutor.new(->(ctx) {
66
- # ctx.output = "Hello, #{ctx.input}!"
67
- # })
68
- #
69
- class LambdaExecutor
70
- # @param callable [#call] A lambda/proc that takes a context
71
- def initialize(callable)
72
- @callable = callable
73
- end
74
-
75
- # Execute the lambda with the context
76
- #
77
- # @param context [Context] The execution context
78
- # @return [Context] The context (possibly modified by the lambda)
79
- def call(context)
80
- @callable.call(context)
81
- context
82
- end
83
- end
84
60
  end
85
61
  end
86
62
  end
@@ -39,13 +39,35 @@ module RubyLLM
39
39
  @app.call(context)
40
40
 
41
41
  # Record spend after successful execution (if not cached)
42
- record_spend!(context) if context.success? && !context.cached?
42
+ if context.success? && !context.cached?
43
+ record_spend!(context)
44
+ emit_budget_notification("ruby_llm_agents.budget.record", context,
45
+ total_cost: context.total_cost,
46
+ total_tokens: context.total_tokens)
47
+ end
43
48
 
44
49
  context
45
50
  end
46
51
 
47
52
  private
48
53
 
54
+ # Emits an AS::Notification for budget events
55
+ #
56
+ # @param event [String] The notification event name
57
+ # @param context [Context] The execution context
58
+ # @param extras [Hash] Additional payload fields
59
+ def emit_budget_notification(event, context, **extras)
60
+ ActiveSupport::Notifications.instrument(
61
+ event,
62
+ {
63
+ agent_type: context.agent_class&.name,
64
+ tenant_id: context.tenant_id
65
+ }.merge(extras)
66
+ )
67
+ rescue
68
+ # Never let notifications break execution
69
+ end
70
+
49
71
  # Returns whether budgets are enabled globally
50
72
  #
51
73
  # @return [Boolean]
@@ -63,6 +85,8 @@ module RubyLLM
63
85
  # @param context [Context] The execution context
64
86
  # @raise [BudgetExceededError] If budget exceeded with hard enforcement
65
87
  def check_budget!(context)
88
+ emit_budget_notification("ruby_llm_agents.budget.check", context)
89
+
66
90
  if context.tenant_id.present?
67
91
  tenant = RubyLLM::Agents::Tenant.find_by(tenant_id: context.tenant_id)
68
92
  if tenant
@@ -77,6 +101,7 @@ module RubyLLM
77
101
  tenant_id: context.tenant_id
78
102
  )
79
103
  rescue RubyLLM::Agents::Reliability::BudgetExceededError
104
+ emit_budget_notification("ruby_llm_agents.budget.exceeded", context)
80
105
  raise
81
106
  rescue => e
82
107
  error("Budget check failed: #{e.message}")
@@ -41,10 +41,13 @@ module RubyLLM
41
41
  context.cached = true
42
42
  context[:cache_key] = cache_key
43
43
  debug("Cache hit for #{cache_key}")
44
+ emit_cache_notification("ruby_llm_agents.cache.hit", cache_key)
44
45
  return context
45
46
  end
46
47
  end
47
48
 
49
+ emit_cache_notification("ruby_llm_agents.cache.miss", cache_key)
50
+
48
51
  # Execute the chain
49
52
  @app.call(context)
50
53
 
@@ -52,6 +55,7 @@ module RubyLLM
52
55
  if context.success?
53
56
  cache_write(cache_key, context.output)
54
57
  debug("Cache write for #{cache_key}")
58
+ emit_cache_notification("ruby_llm_agents.cache.write", cache_key)
55
59
  end
56
60
 
57
61
  context
@@ -59,6 +63,20 @@ module RubyLLM
59
63
 
60
64
  private
61
65
 
66
+ # Emits an AS::Notification for cache events
67
+ #
68
+ # @param event [String] The notification event name
69
+ # @param cache_key [String] The cache key involved
70
+ def emit_cache_notification(event, cache_key)
71
+ ActiveSupport::Notifications.instrument(
72
+ event,
73
+ agent_type: @agent_class&.name,
74
+ cache_key: cache_key
75
+ )
76
+ rescue
77
+ # Never let notifications break execution
78
+ end
79
+
62
80
  # Returns whether caching is enabled for this agent
63
81
  #
64
82
  # @return [Boolean]
@@ -42,6 +42,7 @@ module RubyLLM
42
42
  # Create "running" record immediately (SYNC - must appear on dashboard)
43
43
  execution = create_running_execution(context)
44
44
  context.execution_id = execution&.id
45
+ emit_start_notification(context)
45
46
  status_update_completed = false
46
47
  raised_exception = nil
47
48
 
@@ -55,6 +56,8 @@ module RubyLLM
55
56
  rescue
56
57
  # Let ensure block handle via mark_execution_failed!
57
58
  end
59
+
60
+ emit_complete_notification(context, "success")
58
61
  rescue => e
59
62
  context.completed_at = Time.current
60
63
  context.error = e
@@ -67,6 +70,7 @@ module RubyLLM
67
70
  # Let ensure block handle via mark_execution_failed!
68
71
  end
69
72
 
73
+ emit_complete_notification(context, determine_error_status(e))
70
74
  raise
71
75
  ensure
72
76
  # Emergency fallback if update failed
@@ -93,6 +97,12 @@ module RubyLLM
93
97
  data = build_running_execution_data(context)
94
98
  execution = Execution.create!(data)
95
99
 
100
+ # Root executions point root_execution_id at themselves
101
+ if execution.parent_execution_id.nil? && execution.root_execution_id.nil?
102
+ execution.update_column(:root_execution_id, execution.id)
103
+ end
104
+ context.root_execution_id = execution.root_execution_id || execution.id
105
+
96
106
  # Create detail record with parameters
97
107
  params = sanitize_parameters(context)
98
108
  if params.present? && params != {}
@@ -181,6 +191,61 @@ module RubyLLM
181
191
  error.is_a?(Timeout::Error) ? "timeout" : "error"
182
192
  end
183
193
 
194
+ # Emits an AS::Notification for execution start
195
+ #
196
+ # Fires even when DB tracking is disabled — observability should
197
+ # work independently of persistence.
198
+ #
199
+ # @param context [Context] The execution context
200
+ def emit_start_notification(context)
201
+ ActiveSupport::Notifications.instrument(
202
+ "ruby_llm_agents.execution.start",
203
+ agent_type: context.agent_class&.name,
204
+ model: context.model,
205
+ tenant_id: context.tenant_id,
206
+ execution_id: context.execution_id
207
+ )
208
+ rescue
209
+ # Never let notifications break execution
210
+ end
211
+
212
+ # Emits an AS::Notification for execution completion or error
213
+ #
214
+ # Uses execution.complete for success, execution.error for failures.
215
+ # Fires even when DB tracking is disabled.
216
+ #
217
+ # @param context [Context] The execution context
218
+ # @param status [String] "success", "error", or "timeout"
219
+ def emit_complete_notification(context, status)
220
+ event = (status == "success") ? "ruby_llm_agents.execution.complete" : "ruby_llm_agents.execution.error"
221
+
222
+ ActiveSupport::Notifications.instrument(
223
+ event,
224
+ agent_type: context.agent_class&.name,
225
+ agent_type_symbol: context.agent_type,
226
+ execution_id: context.execution_id,
227
+ model: context.model,
228
+ model_used: context.model_used,
229
+ tenant_id: context.tenant_id,
230
+ status: status,
231
+ duration_ms: context.duration_ms,
232
+ input_tokens: context.input_tokens,
233
+ output_tokens: context.output_tokens,
234
+ total_tokens: context.total_tokens,
235
+ input_cost: context.input_cost,
236
+ output_cost: context.output_cost,
237
+ total_cost: context.total_cost,
238
+ cached: context.cached?,
239
+ attempts_made: context.attempts_made,
240
+ finish_reason: context.finish_reason,
241
+ time_to_first_token_ms: context.time_to_first_token_ms,
242
+ error_class: context.error&.class&.name,
243
+ error_message: context.error&.message
244
+ )
245
+ rescue
246
+ # Never let notifications break execution
247
+ end
248
+
184
249
  # Builds data for initial running execution record
185
250
  #
186
251
  # @param context [Context] The execution context
@@ -202,6 +267,28 @@ module RubyLLM
202
267
  data[:tenant_id] = context.tenant_id
203
268
  end
204
269
 
270
+ # Include agent-defined metadata so it appears on the dashboard immediately
271
+ agent_meta = safe_agent_metadata(context)
272
+ if agent_meta.any?
273
+ data[:metadata] = agent_meta.transform_keys(&:to_s)
274
+ end
275
+
276
+ # Track replay source if this is a replayed execution
277
+ replay_source_id = begin
278
+ context.agent_instance&.send(:options)&.dig(:_replay_source_id)
279
+ rescue
280
+ nil
281
+ end
282
+ if replay_source_id
283
+ data[:metadata] = (data[:metadata] || {}).merge("replay_source_id" => replay_source_id.to_s)
284
+ end
285
+
286
+ # Execution hierarchy (agent-as-tool)
287
+ if context.parent_execution_id.present?
288
+ data[:parent_execution_id] = context.parent_execution_id
289
+ data[:root_execution_id] = context.root_execution_id || context.parent_execution_id
290
+ end
291
+
205
292
  data
206
293
  end
207
294
 
@@ -222,12 +309,18 @@ module RubyLLM
222
309
  attempts_count: context.attempts_made
223
310
  }
224
311
 
225
- # Store niche cache key in metadata
226
- merged_metadata = begin
312
+ # Merge metadata: agent metadata (base) < middleware metadata (overlay)
313
+ agent_meta = safe_agent_metadata(context)
314
+ merged_metadata = agent_meta.transform_keys(&:to_s)
315
+
316
+ context_meta = begin
227
317
  context.metadata.dup
228
318
  rescue
229
319
  {}
230
320
  end
321
+ context_meta.transform_keys!(&:to_s)
322
+ merged_metadata.merge!(context_meta)
323
+
231
324
  if context.cached? && context[:cache_key]
232
325
  merged_metadata["response_cache_key"] = context[:cache_key]
233
326
  end
@@ -321,11 +414,18 @@ module RubyLLM
321
414
  # @param status [String] "success" or "error"
322
415
  # @return [Hash] Execution data
323
416
  def build_execution_data(context, status)
324
- merged_metadata = begin
417
+ # Merge metadata: agent metadata (base) < middleware metadata (overlay)
418
+ agent_meta = safe_agent_metadata(context)
419
+ merged_metadata = agent_meta.transform_keys(&:to_s)
420
+
421
+ context_meta = begin
325
422
  context.metadata.dup
326
423
  rescue
327
424
  {}
328
425
  end
426
+ context_meta.transform_keys!(&:to_s)
427
+ merged_metadata.merge!(context_meta)
428
+
329
429
  if context.cached? && context[:cache_key]
330
430
  merged_metadata["response_cache_key"] = context[:cache_key]
331
431
  end
@@ -423,15 +523,42 @@ module RubyLLM
423
523
  params[key] = "[REDACTED]" if params.key?(key)
424
524
  end
425
525
 
526
+ INTERNAL_KEYS.each do |key|
527
+ params.delete(key)
528
+ end
529
+
426
530
  params
427
531
  end
428
532
 
533
+ # Safely retrieves custom metadata from the agent instance
534
+ #
535
+ # Returns an empty hash if the agent doesn't define metadata,
536
+ # the method raises, or the result isn't a Hash.
537
+ #
538
+ # @param context [Context] The execution context
539
+ # @return [Hash] Agent-defined metadata, or empty hash
540
+ def safe_agent_metadata(context)
541
+ return {} unless context.agent_instance
542
+ return {} unless context.agent_instance.respond_to?(:metadata)
543
+
544
+ result = context.agent_instance.metadata
545
+ result.is_a?(Hash) ? result : {}
546
+ rescue => e
547
+ debug("Failed to retrieve agent metadata: #{e.message}")
548
+ {}
549
+ end
550
+
429
551
  # Sensitive parameter keys that should be redacted
430
552
  SENSITIVE_KEYS = %w[
431
553
  password token api_key secret credential auth key
432
554
  access_token refresh_token private_key secret_key
433
555
  ].freeze
434
556
 
557
+ # Internal keys that should be stripped from persisted parameters
558
+ INTERNAL_KEYS = %w[
559
+ _replay_source_id _ask_message _parent_execution_id _root_execution_id
560
+ ].freeze
561
+
435
562
  # Truncates error message to prevent database issues
436
563
  #
437
564
  # @param message [String] The error message
@@ -120,6 +120,17 @@ module RubyLLM
120
120
 
121
121
  if result
122
122
  context[:reliability_attempts] = tracker.to_json_array
123
+
124
+ # Emit fallback_used if a non-primary model succeeded
125
+ if current_model != models_to_try.first
126
+ emit_reliability_notification(
127
+ "ruby_llm_agents.reliability.fallback_used",
128
+ primary_model: models_to_try.first,
129
+ used_model: current_model,
130
+ attempts_made: context.attempts_made
131
+ )
132
+ end
133
+
123
134
  return result
124
135
  end
125
136
 
@@ -131,6 +142,11 @@ module RubyLLM
131
142
  context[:reliability_attempts] = tracker.to_json_array
132
143
 
133
144
  # All models exhausted
145
+ emit_reliability_notification(
146
+ "ruby_llm_agents.reliability.all_models_exhausted",
147
+ models_tried: models_to_try
148
+ )
149
+
134
150
  raise Agents::Reliability::AllModelsExhaustedError.new(
135
151
  models_to_try, last_error,
136
152
  attempts: tracker.to_json_array
@@ -292,6 +308,19 @@ module RubyLLM
292
308
  )
293
309
  end
294
310
 
311
+ # Emits an AS::Notification for reliability events
312
+ #
313
+ # @param event [String] The notification event name
314
+ # @param extras [Hash] Additional payload fields
315
+ def emit_reliability_notification(event, **extras)
316
+ ActiveSupport::Notifications.instrument(
317
+ event,
318
+ {agent_type: @agent_class&.name}.merge(extras)
319
+ )
320
+ rescue
321
+ # Never let notifications break execution
322
+ end
323
+
295
324
  # Sleeps without blocking other fibers when in async context
296
325
  #
297
326
  # @param seconds [Numeric] Duration to sleep
@@ -59,10 +59,17 @@ module RubyLLM
59
59
  case tenant_value
60
60
  when nil
61
61
  # No explicit tenant - fall back to configured tenant_resolver
62
- resolved_id = RubyLLM::Agents.configuration.current_tenant_id
63
- context.tenant_id = resolved_id&.to_s
64
- context.tenant_object = nil
65
- context.tenant_config = nil
62
+ resolved_value = RubyLLM::Agents.configuration.current_tenant_id
63
+
64
+ if resolved_value.respond_to?(:llm_tenant_id)
65
+ context.tenant_id = resolved_value.llm_tenant_id&.to_s
66
+ context.tenant_object = resolved_value
67
+ context.tenant_config = extract_tenant_config(resolved_value)
68
+ else
69
+ context.tenant_id = resolved_value&.to_s
70
+ context.tenant_object = nil
71
+ context.tenant_config = nil
72
+ end
66
73
 
67
74
  when Hash
68
75
  # Hash format: { id: "tenant_id", object: tenant, ... }
@@ -40,98 +40,6 @@ require_relative "pipeline/middleware/reliability"
40
40
  module RubyLLM
41
41
  module Agents
42
42
  module Pipeline
43
- # Represents an error result from a failed step
44
- #
45
- # Used to track errors that occurred during step execution while
46
- # allowing the workflow to continue (for optional steps).
47
- #
48
- # @api public
49
- class ErrorResult
50
- attr_reader :step_name, :error_class, :error_message
51
-
52
- def initialize(step_name:, error_class:, error_message:)
53
- @step_name = step_name
54
- @error_class = error_class
55
- @error_message = error_message
56
- end
57
-
58
- def content
59
- nil
60
- end
61
-
62
- def success?
63
- false
64
- end
65
-
66
- def error?
67
- true
68
- end
69
-
70
- def skipped?
71
- false
72
- end
73
-
74
- def input_tokens
75
- 0
76
- end
77
-
78
- def output_tokens
79
- 0
80
- end
81
-
82
- def total_tokens
83
- 0
84
- end
85
-
86
- def cached_tokens
87
- 0
88
- end
89
-
90
- def input_cost
91
- 0.0
92
- end
93
-
94
- def output_cost
95
- 0.0
96
- end
97
-
98
- def total_cost
99
- 0.0
100
- end
101
-
102
- def to_h
103
- {
104
- error: true,
105
- step_name: step_name,
106
- error_class: error_class,
107
- error_message: error_message
108
- }
109
- end
110
- end
111
-
112
- class << self
113
- # Build a pipeline for an agent class with default middleware
114
- #
115
- # This is a convenience method that combines Builder.for with build.
116
- #
117
- # @param agent_class [Class] The agent class
118
- # @param executor [#call] The core executor
119
- # @return [#call] The complete pipeline
120
- def build(agent_class, executor)
121
- Builder.for(agent_class).build(executor)
122
- end
123
-
124
- # Build an empty pipeline (no middleware)
125
- #
126
- # Useful for testing or when you want direct execution.
127
- #
128
- # @param agent_class [Class] The agent class
129
- # @param executor [#call] The core executor
130
- # @return [#call] The executor (no middleware wrapping)
131
- def build_empty(agent_class, executor)
132
- Builder.empty(agent_class).build(executor)
133
- end
134
- end
135
43
  end
136
44
  end
137
45
  end
@@ -19,6 +19,7 @@ module RubyLLM
19
19
  :alpha_matting, :refine_edges,
20
20
  :started_at, :completed_at, :tenant_id, :remover_class,
21
21
  :error_class, :error_message
22
+ attr_accessor :execution_id
22
23
 
23
24
  # Initialize a new result
24
25
  #
@@ -51,6 +52,14 @@ module RubyLLM
51
52
  @remover_class = remover_class
52
53
  @error_class = error_class
53
54
  @error_message = error_message
55
+ @execution_id = nil
56
+ end
57
+
58
+ # Loads the associated Execution record from the database
59
+ #
60
+ # @return [RubyLLM::Agents::Execution, nil] The execution record, or nil
61
+ def execution
62
+ @execution ||= RubyLLM::Agents::Execution.find_by(id: execution_id) if execution_id
54
63
  end
55
64
 
56
65
  # Status helpers
@@ -196,7 +205,8 @@ module RubyLLM
196
205
  tenant_id: tenant_id,
197
206
  remover_class: remover_class,
198
207
  error_class: error_class,
199
- error_message: error_message
208
+ error_message: error_message,
209
+ execution_id: execution_id
200
210
  }
201
211
  end
202
212
 
@@ -102,6 +102,11 @@ module RubyLLM
102
102
  # @return [Integer, nil] Number of tokens used for thinking
103
103
  attr_reader :thinking_text, :thinking_signature, :thinking_tokens
104
104
 
105
+ # @!group Execution Record
106
+ # @!attribute [r] execution_id
107
+ # @return [Integer, nil] Database ID of the associated Execution record
108
+ attr_reader :execution_id
109
+
105
110
  # Creates a new Result instance
106
111
  #
107
112
  # @param content [Hash, String] The processed response content
@@ -151,6 +156,22 @@ module RubyLLM
151
156
  @thinking_text = options[:thinking_text]
152
157
  @thinking_signature = options[:thinking_signature]
153
158
  @thinking_tokens = options[:thinking_tokens]
159
+
160
+ # Execution record
161
+ @execution_id = options[:execution_id]
162
+ end
163
+
164
+ # Loads the associated Execution record from the database
165
+ #
166
+ # Useful for debugging in the Rails console to inspect the full
167
+ # execution record after an agent call.
168
+ #
169
+ # @return [RubyLLM::Agents::Execution, nil] The execution record, or nil
170
+ # @example
171
+ # result = MyAgent.call(query: "test")
172
+ # result.execution # => #<RubyLLM::Agents::Execution id: 42, ...>
173
+ def execution
174
+ @execution ||= Execution.find_by(id: execution_id) if execution_id
154
175
  end
155
176
 
156
177
  # Returns total tokens (input + output)
@@ -240,7 +261,8 @@ module RubyLLM
240
261
  tool_calls_count: tool_calls_count,
241
262
  thinking_text: thinking_text,
242
263
  thinking_signature: thinking_signature,
243
- thinking_tokens: thinking_tokens
264
+ thinking_tokens: thinking_tokens,
265
+ execution_id: execution_id
244
266
  }
245
267
  end
246
268
 
@@ -73,6 +73,10 @@ module RubyLLM
73
73
  # @return [String, nil] Exception message if failed
74
74
  attr_reader :error_message
75
75
 
76
+ # @!attribute [r] execution_id
77
+ # @return [Integer, nil] Database ID of the associated Execution record
78
+ attr_reader :execution_id
79
+
76
80
  # Creates a new EmbeddingResult instance
77
81
  #
78
82
  # @param attributes [Hash] Result attributes
@@ -101,6 +105,14 @@ module RubyLLM
101
105
  @tenant_id = attributes[:tenant_id]
102
106
  @error_class = attributes[:error_class]
103
107
  @error_message = attributes[:error_message]
108
+ @execution_id = attributes[:execution_id]
109
+ end
110
+
111
+ # Loads the associated Execution record from the database
112
+ #
113
+ # @return [RubyLLM::Agents::Execution, nil] The execution record, or nil
114
+ def execution
115
+ @execution ||= RubyLLM::Agents::Execution.find_by(id: execution_id) if execution_id
104
116
  end
105
117
 
106
118
  # Returns whether this result contains a single embedding
@@ -215,7 +227,8 @@ module RubyLLM
215
227
  completed_at: completed_at,
216
228
  tenant_id: tenant_id,
217
229
  error_class: error_class,
218
- error_message: error_message
230
+ error_message: error_message,
231
+ execution_id: execution_id
219
232
  }
220
233
  end
221
234
 
@@ -21,6 +21,7 @@ module RubyLLM
21
21
  :caption, :description, :tags, :objects, :colors, :text,
22
22
  :raw_response, :started_at, :completed_at, :tenant_id, :analyzer_class,
23
23
  :error_class, :error_message
24
+ attr_accessor :execution_id
24
25
 
25
26
  # Initialize a new result
26
27
  #
@@ -59,6 +60,14 @@ module RubyLLM
59
60
  @analyzer_class = analyzer_class
60
61
  @error_class = error_class
61
62
  @error_message = error_message
63
+ @execution_id = nil
64
+ end
65
+
66
+ # Loads the associated Execution record from the database
67
+ #
68
+ # @return [RubyLLM::Agents::Execution, nil] The execution record, or nil
69
+ def execution
70
+ @execution ||= RubyLLM::Agents::Execution.find_by(id: execution_id) if execution_id
62
71
  end
63
72
 
64
73
  # Status helpers
@@ -205,7 +214,8 @@ module RubyLLM
205
214
  tenant_id: tenant_id,
206
215
  analyzer_class: analyzer_class,
207
216
  error_class: error_class,
208
- error_message: error_message
217
+ error_message: error_message,
218
+ execution_id: execution_id
209
219
  }
210
220
  end
211
221
 
@@ -20,6 +20,7 @@ module RubyLLM
20
20
  attr_reader :images, :source_image, :mask, :prompt, :model_id, :size,
21
21
  :started_at, :completed_at, :tenant_id, :editor_class,
22
22
  :error_class, :error_message
23
+ attr_accessor :execution_id
23
24
 
24
25
  # Initialize a new result
25
26
  #
@@ -50,6 +51,14 @@ module RubyLLM
50
51
  @editor_class = editor_class
51
52
  @error_class = error_class
52
53
  @error_message = error_message
54
+ @execution_id = nil
55
+ end
56
+
57
+ # Loads the associated Execution record from the database
58
+ #
59
+ # @return [RubyLLM::Agents::Execution, nil] The execution record, or nil
60
+ def execution
61
+ @execution ||= RubyLLM::Agents::Execution.find_by(id: execution_id) if execution_id
53
62
  end
54
63
 
55
64
  # Status helpers
@@ -175,7 +184,8 @@ module RubyLLM
175
184
  tenant_id: tenant_id,
176
185
  editor_class: editor_class,
177
186
  error_class: error_class,
178
- error_message: error_message
187
+ error_message: error_message,
188
+ execution_id: execution_id
179
189
  }
180
190
  end
181
191