ruby_llm-agents 1.3.3 → 2.0.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 (192) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +101 -334
  3. data/app/controllers/concerns/ruby_llm/agents/sortable.rb +0 -1
  4. data/app/controllers/ruby_llm/agents/agents_controller.rb +5 -56
  5. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +22 -106
  6. data/app/controllers/ruby_llm/agents/executions_controller.rb +4 -114
  7. data/app/controllers/ruby_llm/agents/tenants_controller.rb +30 -2
  8. data/app/helpers/ruby_llm/agents/application_helper.rb +19 -53
  9. data/app/models/ruby_llm/agents/execution/analytics.rb +13 -54
  10. data/app/models/ruby_llm/agents/execution/scopes.rb +61 -14
  11. data/app/models/ruby_llm/agents/execution.rb +46 -10
  12. data/app/models/ruby_llm/agents/execution_detail.rb +18 -0
  13. data/app/models/ruby_llm/agents/tenant/budgetable.rb +132 -24
  14. data/app/models/ruby_llm/agents/tenant/incrementable.rb +117 -0
  15. data/app/models/ruby_llm/agents/tenant/resettable.rb +128 -0
  16. data/app/models/ruby_llm/agents/tenant/trackable.rb +46 -12
  17. data/app/models/ruby_llm/agents/tenant.rb +2 -3
  18. data/app/models/ruby_llm/agents/tenant_budget.rb +6 -3
  19. data/app/services/ruby_llm/agents/agent_registry.rb +6 -112
  20. data/app/views/layouts/ruby_llm/agents/application.html.erb +87 -252
  21. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +71 -218
  22. data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +20 -63
  23. data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +44 -131
  24. data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +16 -57
  25. data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +39 -104
  26. data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +29 -82
  27. data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +4 -14
  28. data/app/views/ruby_llm/agents/agents/index.html.erb +105 -274
  29. data/app/views/ruby_llm/agents/agents/show.html.erb +248 -378
  30. data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +29 -52
  31. data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +73 -99
  32. data/app/views/ruby_llm/agents/dashboard/index.html.erb +228 -433
  33. data/app/views/ruby_llm/agents/executions/_execution.html.erb +1 -1
  34. data/app/views/ruby_llm/agents/executions/_filters.html.erb +4 -25
  35. data/app/views/ruby_llm/agents/executions/_list.html.erb +111 -152
  36. data/app/views/ruby_llm/agents/executions/index.html.erb +5 -7
  37. data/app/views/ruby_llm/agents/executions/show.html.erb +528 -989
  38. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +5 -21
  39. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +70 -191
  40. data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +16 -44
  41. data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +12 -41
  42. data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +11 -65
  43. data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +6 -5
  44. data/app/views/ruby_llm/agents/system_config/show.html.erb +240 -351
  45. data/app/views/ruby_llm/agents/tenants/_form.html.erb +67 -77
  46. data/app/views/ruby_llm/agents/tenants/edit.html.erb +7 -9
  47. data/app/views/ruby_llm/agents/tenants/index.html.erb +100 -122
  48. data/app/views/ruby_llm/agents/tenants/show.html.erb +146 -336
  49. data/config/routes.rb +0 -13
  50. data/lib/generators/ruby_llm_agents/install_generator.rb +9 -14
  51. data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +2 -12
  52. data/lib/generators/ruby_llm_agents/restructure_generator.rb +0 -2
  53. data/lib/generators/ruby_llm_agents/templates/add_usage_counters_to_tenants_migration.rb.tt +37 -0
  54. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +1 -2
  55. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +1 -1
  56. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +0 -1
  57. data/lib/generators/ruby_llm_agents/templates/create_execution_details_migration.rb.tt +27 -0
  58. data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +25 -0
  59. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +0 -1
  60. data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +9 -12
  61. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +40 -71
  62. data/lib/generators/ruby_llm_agents/templates/remove_agent_version_migration.rb.tt +13 -0
  63. data/lib/generators/ruby_llm_agents/templates/remove_workflow_columns_migration.rb.tt +19 -0
  64. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +2 -4
  65. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +0 -1
  66. data/lib/generators/ruby_llm_agents/templates/split_execution_details_migration.rb.tt +232 -0
  67. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +58 -262
  68. data/lib/ruby_llm/agents/audio/speaker.rb +0 -1
  69. data/lib/ruby_llm/agents/audio/transcriber.rb +0 -1
  70. data/lib/ruby_llm/agents/base_agent.rb +52 -6
  71. data/lib/ruby_llm/agents/core/base/callbacks.rb +142 -0
  72. data/lib/ruby_llm/agents/core/base.rb +23 -55
  73. data/lib/ruby_llm/agents/core/configuration.rb +58 -117
  74. data/lib/ruby_llm/agents/core/errors.rb +0 -58
  75. data/lib/ruby_llm/agents/core/instrumentation.rb +157 -110
  76. data/lib/ruby_llm/agents/core/llm_tenant.rb +8 -7
  77. data/lib/ruby_llm/agents/core/version.rb +1 -1
  78. data/lib/ruby_llm/agents/dsl/base.rb +157 -17
  79. data/lib/ruby_llm/agents/dsl/caching.rb +33 -2
  80. data/lib/ruby_llm/agents/dsl/reliability.rb +148 -0
  81. data/lib/ruby_llm/agents/dsl.rb +1 -2
  82. data/lib/ruby_llm/agents/image/analyzer/execution.rb +1 -2
  83. data/lib/ruby_llm/agents/image/background_remover/execution.rb +1 -2
  84. data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +1 -13
  85. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +2 -2
  86. data/lib/ruby_llm/agents/image/editor/dsl.rb +0 -14
  87. data/lib/ruby_llm/agents/image/editor/execution.rb +1 -10
  88. data/lib/ruby_llm/agents/image/editor.rb +0 -1
  89. data/lib/ruby_llm/agents/image/generator.rb +0 -21
  90. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +0 -13
  91. data/lib/ruby_llm/agents/image/pipeline/execution.rb +0 -1
  92. data/lib/ruby_llm/agents/image/transformer/dsl.rb +0 -13
  93. data/lib/ruby_llm/agents/image/transformer/execution.rb +1 -10
  94. data/lib/ruby_llm/agents/image/transformer.rb +0 -1
  95. data/lib/ruby_llm/agents/image/upscaler/execution.rb +1 -2
  96. data/lib/ruby_llm/agents/image/variator/execution.rb +1 -2
  97. data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +78 -173
  98. data/lib/ruby_llm/agents/infrastructure/attempt_tracker.rb +1 -0
  99. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +66 -2
  100. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +0 -12
  101. data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +10 -13
  102. data/lib/ruby_llm/agents/infrastructure/reliability.rb +37 -2
  103. data/lib/ruby_llm/agents/pipeline/context.rb +0 -1
  104. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +28 -4
  105. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +3 -10
  106. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +88 -55
  107. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +5 -41
  108. data/lib/ruby_llm/agents/rails/engine.rb +6 -6
  109. data/lib/ruby_llm/agents/results/base.rb +1 -49
  110. data/lib/ruby_llm/agents/text/embedder.rb +0 -1
  111. data/lib/ruby_llm/agents.rb +1 -9
  112. data/lib/tasks/ruby_llm_agents.rake +34 -0
  113. metadata +12 -81
  114. data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +0 -214
  115. data/app/controllers/ruby_llm/agents/workflows_controller.rb +0 -544
  116. data/app/mailers/ruby_llm/agents/alert_mailer.rb +0 -84
  117. data/app/mailers/ruby_llm/agents/application_mailer.rb +0 -28
  118. data/app/models/ruby_llm/agents/api_configuration.rb +0 -386
  119. data/app/models/ruby_llm/agents/execution/workflow.rb +0 -170
  120. data/app/models/ruby_llm/agents/tenant/configurable.rb +0 -135
  121. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -98
  122. data/app/views/ruby_llm/agents/agents/_version_comparison.html.erb +0 -186
  123. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +0 -126
  124. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +0 -107
  125. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +0 -18
  126. data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +0 -34
  127. data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +0 -288
  128. data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +0 -95
  129. data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +0 -97
  130. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +0 -214
  131. data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +0 -179
  132. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +0 -73
  133. data/app/views/ruby_llm/agents/dashboard/_alerts_feed.html.erb +0 -62
  134. data/app/views/ruby_llm/agents/dashboard/_breaker_strip.html.erb +0 -47
  135. data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +0 -75
  136. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +0 -56
  137. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +0 -115
  138. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +0 -59
  139. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +0 -60
  140. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +0 -86
  141. data/app/views/ruby_llm/agents/executions/dry_run.html.erb +0 -149
  142. data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +0 -48
  143. data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +0 -27
  144. data/app/views/ruby_llm/agents/shared/_stat_card.html.erb +0 -14
  145. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +0 -35
  146. data/app/views/ruby_llm/agents/workflows/_empty_state.html.erb +0 -22
  147. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +0 -228
  148. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +0 -539
  149. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +0 -76
  150. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +0 -74
  151. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +0 -108
  152. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +0 -920
  153. data/app/views/ruby_llm/agents/workflows/index.html.erb +0 -179
  154. data/app/views/ruby_llm/agents/workflows/show.html.erb +0 -467
  155. data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +0 -100
  156. data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +0 -38
  157. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +0 -48
  158. data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +0 -90
  159. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +0 -551
  160. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +0 -181
  161. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +0 -274
  162. data/lib/ruby_llm/agents/core/resolved_config.rb +0 -348
  163. data/lib/ruby_llm/agents/image/generator/content_policy.rb +0 -95
  164. data/lib/ruby_llm/agents/infrastructure/redactor.rb +0 -130
  165. data/lib/ruby_llm/agents/results/moderation_result.rb +0 -158
  166. data/lib/ruby_llm/agents/text/moderator.rb +0 -237
  167. data/lib/ruby_llm/agents/workflow/approval.rb +0 -205
  168. data/lib/ruby_llm/agents/workflow/approval_store.rb +0 -179
  169. data/lib/ruby_llm/agents/workflow/async.rb +0 -220
  170. data/lib/ruby_llm/agents/workflow/async_executor.rb +0 -156
  171. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +0 -467
  172. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +0 -244
  173. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +0 -289
  174. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +0 -107
  175. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +0 -150
  176. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +0 -187
  177. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +0 -352
  178. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +0 -415
  179. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +0 -257
  180. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +0 -317
  181. data/lib/ruby_llm/agents/workflow/dsl.rb +0 -576
  182. data/lib/ruby_llm/agents/workflow/instrumentation.rb +0 -249
  183. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +0 -117
  184. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +0 -117
  185. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +0 -180
  186. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +0 -121
  187. data/lib/ruby_llm/agents/workflow/notifiers.rb +0 -70
  188. data/lib/ruby_llm/agents/workflow/orchestrator.rb +0 -416
  189. data/lib/ruby_llm/agents/workflow/result.rb +0 -592
  190. data/lib/ruby_llm/agents/workflow/thread_pool.rb +0 -185
  191. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +0 -206
  192. data/lib/ruby_llm/agents/workflow/wait_result.rb +0 -213
@@ -88,63 +88,5 @@ module RubyLLM
88
88
 
89
89
  # Raised for configuration issues
90
90
  class ConfigurationError < Error; end
91
-
92
- # Raised when content is flagged during moderation
93
- #
94
- # Contains the full moderation result and the phase where
95
- # the content was flagged.
96
- #
97
- # @example Handling moderation errors
98
- # begin
99
- # result = MyAgent.call(message: user_input)
100
- # rescue RubyLLM::Agents::ModerationError => e
101
- # puts "Content blocked: #{e.flagged_categories.join(', ')}"
102
- # puts "Phase: #{e.phase}"
103
- # puts "Scores: #{e.category_scores}"
104
- # end
105
- #
106
- # @api public
107
- class ModerationError < Error
108
- # @return [Object] The raw moderation result from RubyLLM
109
- attr_reader :moderation_result
110
-
111
- # @return [Symbol] The phase where content was flagged (:input or :output)
112
- attr_reader :phase
113
-
114
- # Creates a new ModerationError
115
- #
116
- # @param moderation_result [Object] The moderation result from RubyLLM
117
- # @param phase [Symbol] The phase where content was flagged
118
- def initialize(moderation_result, phase)
119
- @moderation_result = moderation_result
120
- @phase = phase
121
-
122
- categories = moderation_result.flagged_categories
123
- category_list = categories.respond_to?(:join) ? categories.join(", ") : categories.to_s
124
-
125
- super("Content flagged during #{phase} moderation: #{category_list}")
126
- end
127
-
128
- # Returns the flagged categories from the moderation result
129
- #
130
- # @return [Array<String, Symbol>] List of flagged categories
131
- def flagged_categories
132
- moderation_result.flagged_categories
133
- end
134
-
135
- # Returns the category scores from the moderation result
136
- #
137
- # @return [Hash{String, Symbol => Float}] Category to score mapping
138
- def category_scores
139
- moderation_result.category_scores
140
- end
141
-
142
- # Returns whether the moderation result was flagged
143
- #
144
- # @return [Boolean] Always true for ModerationError
145
- def flagged?
146
- true
147
- end
148
- end
149
91
  end
150
92
  end
@@ -15,7 +15,7 @@ module RubyLLM
15
15
  #
16
16
  # @example Adding custom metadata to executions
17
17
  # class MyAgent < ApplicationAgent
18
- # def execution_metadata
18
+ # def metadata
19
19
  # { user_id: Current.user&.id, request_id: request.uuid }
20
20
  # end
21
21
  # end
@@ -232,36 +232,33 @@ module RubyLLM
232
232
  # @return [RubyLLM::Agents::Execution, nil] The created record, or nil on failure
233
233
  def create_running_execution(started_at, fallback_chain: [])
234
234
  config = RubyLLM::Agents.configuration
235
- metadata = execution_metadata
235
+ agent_metadata = metadata
236
+
237
+ # Separate niche tracing fields into metadata
238
+ exec_metadata = agent_metadata.dup
239
+ exec_metadata["span_id"] = exec_metadata.delete(:span_id) if exec_metadata[:span_id]
236
240
 
237
241
  execution_data = {
238
242
  agent_type: self.class.name,
239
- agent_version: self.class.version,
240
243
  model_id: model,
241
244
  temperature: temperature,
242
245
  started_at: started_at,
243
246
  status: "running",
244
- parameters: redacted_parameters,
245
- metadata: metadata,
246
- system_prompt: config.persist_prompts ? redacted_system_prompt : nil,
247
- user_prompt: config.persist_prompts ? redacted_user_prompt : nil,
247
+ metadata: exec_metadata,
248
248
  streaming: self.class.streaming,
249
- messages_count: resolved_messages.size,
250
- messages_summary: config.persist_messages_summary ? messages_summary : {}
249
+ messages_count: resolved_messages.size
251
250
  }
252
251
 
253
252
  # Extract tracing fields from metadata if present
254
- execution_data[:request_id] = metadata[:request_id] if metadata[:request_id]
255
- execution_data[:trace_id] = metadata[:trace_id] if metadata[:trace_id]
256
- execution_data[:span_id] = metadata[:span_id] if metadata[:span_id]
257
- execution_data[:parent_execution_id] = metadata[:parent_execution_id] if metadata[:parent_execution_id]
258
- execution_data[:root_execution_id] = metadata[:root_execution_id] if metadata[:root_execution_id]
253
+ execution_data[:request_id] = agent_metadata[:request_id] if agent_metadata[:request_id]
254
+ execution_data[:trace_id] = agent_metadata[:trace_id] if agent_metadata[:trace_id]
255
+ execution_data[:parent_execution_id] = agent_metadata[:parent_execution_id] if agent_metadata[:parent_execution_id]
256
+ execution_data[:root_execution_id] = agent_metadata[:root_execution_id] if agent_metadata[:root_execution_id]
259
257
 
260
- # Add fallback chain if provided (for reliability-enabled executions)
258
+ # Add fallback chain tracking (count only on execution, chain stored in detail)
261
259
  if fallback_chain.any?
262
- execution_data[:fallback_chain] = fallback_chain
263
- execution_data[:attempts] = []
264
260
  execution_data[:attempts_count] = 0
261
+ @_pending_detail_data = { fallback_chain: fallback_chain, attempts: [] }
265
262
  end
266
263
 
267
264
  # Add tenant_id if multi-tenancy is enabled
@@ -269,7 +266,22 @@ module RubyLLM
269
266
  execution_data[:tenant_id] = config.current_tenant_id
270
267
  end
271
268
 
272
- RubyLLM::Agents::Execution.create!(execution_data)
269
+ execution = RubyLLM::Agents::Execution.create!(execution_data)
270
+
271
+ # Create detail record with prompts and parameters
272
+ detail_data = {
273
+ parameters: sanitized_parameters,
274
+ messages_summary: config.persist_messages_summary ? messages_summary : {},
275
+ system_prompt: config.persist_prompts ? stored_system_prompt : nil,
276
+ user_prompt: config.persist_prompts ? stored_user_prompt : nil
277
+ }
278
+ detail_data.merge!(@_pending_detail_data) if @_pending_detail_data
279
+ @_pending_detail_data = nil
280
+
281
+ has_data = detail_data.values.any? { |v| v.present? && v != {} && v != [] }
282
+ execution.create_detail!(detail_data) if has_data
283
+
284
+ execution
273
285
  rescue StandardError => e
274
286
  # Log error but don't fail the agent execution itself
275
287
  Rails.logger.error("[RubyLLM::Agents] Failed to create execution record: #{e.message}")
@@ -299,26 +311,43 @@ module RubyLLM
299
311
  status: status
300
312
  }
301
313
 
302
- # Add streaming metrics if available
303
- update_data[:time_to_first_token_ms] = time_to_first_token_ms if respond_to?(:time_to_first_token_ms) && time_to_first_token_ms
314
+ # Store niche streaming metrics in metadata
315
+ if respond_to?(:time_to_first_token_ms) && time_to_first_token_ms
316
+ update_data[:metadata] = (execution.metadata || {}).merge("time_to_first_token_ms" => time_to_first_token_ms)
317
+ end
304
318
 
305
319
  # Add response data if available (using safe extraction)
306
320
  response_data = safe_extract_response_data(response)
307
321
  if response_data.any?
308
- update_data.merge!(response_data)
322
+ # Separate execution-level fields from detail-level fields
323
+ detail_fields = response_data.extract!(:response, :tool_calls, :cache_creation_tokens)
324
+ update_data.merge!(response_data.except(:tool_calls_count))
325
+ update_data[:tool_calls_count] = detail_fields[:tool_calls]&.size || 0
309
326
  update_data[:model_id] ||= model
310
327
  end
311
328
 
312
- # Add error data if failed
329
+ # Add error class on execution (error_message goes to detail)
313
330
  if error
314
- update_data.merge!(
315
- error_message: error.message,
316
- error_class: error.class.name
317
- )
331
+ update_data[:error_class] = error.class.name
318
332
  end
319
333
 
320
334
  execution.update!(update_data)
321
335
 
336
+ # Update or create detail record with completion data
337
+ detail_update = {}
338
+ detail_update[:response] = detail_fields[:response] if detail_fields&.dig(:response)
339
+ detail_update[:tool_calls] = detail_fields[:tool_calls] if detail_fields&.dig(:tool_calls)
340
+ detail_update[:cache_creation_tokens] = detail_fields[:cache_creation_tokens] if detail_fields&.dig(:cache_creation_tokens)
341
+ detail_update[:error_message] = error.message if error
342
+
343
+ if detail_update.values.any?(&:present?)
344
+ if execution.detail
345
+ execution.detail.update!(detail_update)
346
+ else
347
+ execution.create_detail!(detail_update)
348
+ end
349
+ end
350
+
322
351
  # Calculate costs if token data is available
323
352
  if execution.input_tokens && execution.output_tokens
324
353
  begin
@@ -368,7 +397,6 @@ module RubyLLM
368
397
  completed_at: completed_at,
369
398
  duration_ms: duration_ms,
370
399
  status: status,
371
- attempts: attempt_tracker.to_json_array,
372
400
  attempts_count: attempt_tracker.attempts_count,
373
401
  chosen_model_id: attempt_tracker.chosen_model_id,
374
402
  input_tokens: attempt_tracker.total_input_tokens,
@@ -377,8 +405,11 @@ module RubyLLM
377
405
  cached_tokens: attempt_tracker.total_cached_tokens
378
406
  }
379
407
 
380
- # Add streaming metrics if available
381
- update_data[:time_to_first_token_ms] = time_to_first_token_ms if respond_to?(:time_to_first_token_ms) && time_to_first_token_ms
408
+ # Store niche streaming metrics in metadata
409
+ merged_metadata = execution.metadata || {}
410
+ if respond_to?(:time_to_first_token_ms) && time_to_first_token_ms
411
+ merged_metadata["time_to_first_token_ms"] = time_to_first_token_ms
412
+ end
382
413
 
383
414
  # Add finish reason from response if available
384
415
  if @last_response
@@ -386,31 +417,46 @@ module RubyLLM
386
417
  update_data[:finish_reason] = finish_reason if finish_reason
387
418
  end
388
419
 
389
- # Add routing/retry tracking fields
420
+ # Store routing/retry niche fields in metadata
390
421
  routing_data = extract_routing_data(attempt_tracker, error)
391
- update_data.merge!(routing_data)
422
+ merged_metadata["fallback_reason"] = routing_data[:fallback_reason] if routing_data[:fallback_reason]
423
+ merged_metadata["retryable"] = routing_data[:retryable] if routing_data.key?(:retryable)
424
+ merged_metadata["rate_limited"] = routing_data[:rate_limited] if routing_data.key?(:rate_limited)
392
425
 
393
- # Add response data if we have a last response
394
- if @last_response && config.persist_responses
395
- update_data[:response] = redacted_response(@last_response)
396
- end
426
+ update_data[:metadata] = merged_metadata if merged_metadata.any?
397
427
 
398
- # Add tool calls from accumulated_tool_calls (captured from all responses)
428
+ # Tool calls count on execution
399
429
  if respond_to?(:accumulated_tool_calls) && accumulated_tool_calls.present?
400
- update_data[:tool_calls] = accumulated_tool_calls
401
430
  update_data[:tool_calls_count] = accumulated_tool_calls.size
402
431
  end
403
432
 
404
- # Add error data if failed
433
+ # Error class on execution (error_message goes to detail)
405
434
  if error
406
- update_data.merge!(
407
- error_message: error.message.to_s.truncate(65535),
408
- error_class: error.class.name
409
- )
435
+ update_data[:error_class] = error.class.name
410
436
  end
411
437
 
412
438
  execution.update!(update_data)
413
439
 
440
+ # Update or create detail record
441
+ detail_update = {
442
+ attempts: attempt_tracker.to_json_array
443
+ }
444
+ if @last_response && config.persist_responses
445
+ detail_update[:response] = stored_response(@last_response)
446
+ end
447
+ if respond_to?(:accumulated_tool_calls) && accumulated_tool_calls.present?
448
+ detail_update[:tool_calls] = accumulated_tool_calls
449
+ end
450
+ if error
451
+ detail_update[:error_message] = error.message.to_s.truncate(65535)
452
+ end
453
+
454
+ if execution.detail
455
+ execution.detail.update!(detail_update)
456
+ else
457
+ execution.create_detail!(detail_update)
458
+ end
459
+
414
460
  # Calculate costs from all attempts
415
461
  if attempt_tracker.attempts_count > 0
416
462
  begin
@@ -452,35 +498,40 @@ module RubyLLM
452
498
 
453
499
  execution_data = {
454
500
  agent_type: self.class.name,
455
- agent_version: self.class.version,
456
501
  model_id: model,
457
502
  temperature: temperature,
458
503
  started_at: Time.current,
459
504
  completed_at: completed_at,
460
505
  duration_ms: 0,
461
506
  status: status,
462
- parameters: sanitized_parameters,
463
- metadata: execution_metadata,
464
- system_prompt: safe_system_prompt,
465
- user_prompt: safe_user_prompt,
466
- messages_count: resolved_messages.size,
467
- messages_summary: config.persist_messages_summary ? messages_summary : {}
507
+ metadata: metadata,
508
+ messages_count: resolved_messages.size
468
509
  }
469
510
 
470
511
  # Add response data if available (using safe extraction)
471
512
  response_data = safe_extract_response_data(response)
472
513
  if response_data.any?
473
- execution_data.merge!(response_data)
514
+ detail_fields = response_data.extract!(:response, :tool_calls, :cache_creation_tokens)
515
+ execution_data.merge!(response_data.except(:tool_calls_count))
516
+ execution_data[:tool_calls_count] = detail_fields[:tool_calls]&.size || 0
474
517
  execution_data[:model_id] ||= model
475
518
  end
476
519
 
477
520
  if error
478
- execution_data.merge!(
479
- error_message: error.message,
480
- error_class: error.class.name
481
- )
521
+ execution_data[:error_class] = error.class.name
482
522
  end
483
523
 
524
+ # Detail data stored separately
525
+ detail_data = {
526
+ parameters: sanitized_parameters,
527
+ system_prompt: safe_system_prompt,
528
+ user_prompt: safe_user_prompt,
529
+ messages_summary: config.persist_messages_summary ? messages_summary : {},
530
+ error_message: error&.message
531
+ }.merge(detail_fields || {})
532
+
533
+ execution_data[:_detail_data] = detail_data
534
+
484
535
  if RubyLLM::Agents.configuration.async_logging
485
536
  RubyLLM::Agents::ExecutionLoggerJob.perform_later(execution_data)
486
537
  else
@@ -488,44 +539,25 @@ module RubyLLM
488
539
  end
489
540
  end
490
541
 
491
- # Sanitizes parameters by removing sensitive data
542
+ # Sanitizes parameters by removing internal options
492
543
  #
493
- # @deprecated Use {#redacted_parameters} instead
494
- # @return [Hash] Sanitized parameters safe for logging
544
+ # @return [Hash] Parameters safe for logging
495
545
  def sanitized_parameters
496
- redacted_parameters
546
+ @options.except(:skip_cache, :dry_run)
497
547
  end
498
548
 
499
- # Returns parameters with sensitive data redacted using the Redactor
549
+ # Returns the system prompt for storage
500
550
  #
501
- # Uses the configured redaction rules to remove sensitive fields and
502
- # apply pattern-based redaction. Also converts ActiveRecord objects
503
- # to ID references.
504
- #
505
- # @return [Hash] Redacted parameters safe for logging
506
- def redacted_parameters
507
- params = @options.except(:skip_cache, :dry_run)
508
- Redactor.redact(params)
551
+ # @return [String, nil] The system prompt
552
+ def stored_system_prompt
553
+ safe_system_prompt
509
554
  end
510
555
 
511
- # Returns the system prompt with redaction applied
556
+ # Returns the user prompt for storage
512
557
  #
513
- # @return [String, nil] The redacted system prompt
514
- def redacted_system_prompt
515
- prompt = safe_system_prompt
516
- return nil unless prompt
517
-
518
- Redactor.redact_string(prompt)
519
- end
520
-
521
- # Returns the user prompt with redaction applied
522
- #
523
- # @return [String, nil] The redacted user prompt
524
- def redacted_user_prompt
525
- prompt = safe_user_prompt
526
- return nil unless prompt
527
-
528
- Redactor.redact_string(prompt)
558
+ # @return [String, nil] The user prompt
559
+ def stored_user_prompt
560
+ safe_user_prompt
529
561
  end
530
562
 
531
563
  # Returns a summary of messages (first and last, truncated)
@@ -545,7 +577,7 @@ module RubyLLM
545
577
  if msgs.first
546
578
  summary[:first] = {
547
579
  role: msgs.first[:role].to_s,
548
- content: Redactor.redact_string(msgs.first[:content].to_s).truncate(max_len)
580
+ content: msgs.first[:content].to_s.truncate(max_len)
549
581
  }
550
582
  end
551
583
 
@@ -553,20 +585,19 @@ module RubyLLM
553
585
  if msgs.size > 1 && msgs.last
554
586
  summary[:last] = {
555
587
  role: msgs.last[:role].to_s,
556
- content: Redactor.redact_string(msgs.last[:content].to_s).truncate(max_len)
588
+ content: msgs.last[:content].to_s.truncate(max_len)
557
589
  }
558
590
  end
559
591
 
560
592
  summary
561
593
  end
562
594
 
563
- # Returns the response with redaction applied
595
+ # Returns the response for storage
564
596
  #
565
597
  # @param response [RubyLLM::Message] The LLM response
566
- # @return [Hash] Redacted response data
567
- def redacted_response(response)
568
- data = safe_serialize_response(response)
569
- Redactor.redact(data)
598
+ # @return [Hash] Response data
599
+ def stored_response(response)
600
+ safe_serialize_response(response)
570
601
  end
571
602
 
572
603
  # Hook for subclasses to add custom metadata to executions
@@ -576,10 +607,10 @@ module RubyLLM
576
607
  #
577
608
  # @return [Hash] Custom metadata to store with the execution
578
609
  # @example
579
- # def execution_metadata
610
+ # def metadata
580
611
  # { user_id: Current.user&.id, experiment: "v2" }
581
612
  # end
582
- def execution_metadata
613
+ def metadata
583
614
  {}
584
615
  end
585
616
 
@@ -816,40 +847,37 @@ module RubyLLM
816
847
  completed_at = Time.current
817
848
  duration_ms = ((completed_at - started_at) * 1000).round
818
849
 
850
+ exec_metadata = metadata.dup
851
+ exec_metadata["response_cache_key"] = cache_key
852
+ exec_metadata["span_id"] = exec_metadata.delete(:span_id) if exec_metadata[:span_id]
853
+
819
854
  execution_data = {
820
855
  agent_type: self.class.name,
821
- agent_version: self.class.version,
822
856
  model_id: model,
823
857
  temperature: temperature,
824
858
  status: "success",
825
859
  cache_hit: true,
826
- response_cache_key: cache_key,
827
- cached_at: completed_at,
828
860
  started_at: started_at,
829
861
  completed_at: completed_at,
830
862
  duration_ms: duration_ms,
831
863
  input_tokens: 0,
832
864
  output_tokens: 0,
833
865
  cached_tokens: 0,
834
- cache_creation_tokens: 0,
835
866
  total_tokens: 0,
836
867
  input_cost: 0,
837
868
  output_cost: 0,
838
869
  total_cost: 0,
839
- parameters: redacted_parameters,
840
- metadata: execution_metadata,
870
+ metadata: exec_metadata,
841
871
  streaming: self.class.streaming,
842
- messages_count: resolved_messages.size,
843
- messages_summary: config.persist_messages_summary ? messages_summary : {}
872
+ messages_count: resolved_messages.size
844
873
  }
845
874
 
846
875
  # Add tracing fields from metadata if present
847
- metadata = execution_metadata
848
- execution_data[:request_id] = metadata[:request_id] if metadata[:request_id]
849
- execution_data[:trace_id] = metadata[:trace_id] if metadata[:trace_id]
850
- execution_data[:span_id] = metadata[:span_id] if metadata[:span_id]
851
- execution_data[:parent_execution_id] = metadata[:parent_execution_id] if metadata[:parent_execution_id]
852
- execution_data[:root_execution_id] = metadata[:root_execution_id] if metadata[:root_execution_id]
876
+ agent_metadata = metadata
877
+ execution_data[:request_id] = agent_metadata[:request_id] if agent_metadata[:request_id]
878
+ execution_data[:trace_id] = agent_metadata[:trace_id] if agent_metadata[:trace_id]
879
+ execution_data[:parent_execution_id] = agent_metadata[:parent_execution_id] if agent_metadata[:parent_execution_id]
880
+ execution_data[:root_execution_id] = agent_metadata[:root_execution_id] if agent_metadata[:root_execution_id]
853
881
 
854
882
  # Add tenant_id if multi-tenancy is enabled
855
883
  if config.multi_tenancy_enabled?
@@ -859,7 +887,15 @@ module RubyLLM
859
887
  if config.async_logging
860
888
  RubyLLM::Agents::ExecutionLoggerJob.perform_later(execution_data)
861
889
  else
862
- RubyLLM::Agents::Execution.create!(execution_data)
890
+ execution = RubyLLM::Agents::Execution.create!(execution_data)
891
+ # Create detail with cache-related fields
892
+ detail_data = {
893
+ parameters: sanitized_parameters,
894
+ messages_summary: config.persist_messages_summary ? messages_summary : {},
895
+ cached_at: completed_at,
896
+ cache_creation_tokens: 0
897
+ }
898
+ execution.create_detail!(detail_data) if detail_data.values.any?(&:present?)
863
899
  end
864
900
  rescue StandardError => e
865
901
  Rails.logger.error("[RubyLLM::Agents] Failed to record cache hit execution: #{e.message}")
@@ -920,11 +956,22 @@ module RubyLLM
920
956
  update_data = {
921
957
  status: "error",
922
958
  completed_at: Time.current,
923
- error_class: error.class.name,
924
- error_message: error_message.to_s.truncate(65535)
959
+ error_class: error.class.name
925
960
  }
926
961
 
927
962
  execution.class.where(id: execution.id, status: "running").update_all(update_data)
963
+
964
+ # Store error_message in detail table (best-effort)
965
+ begin
966
+ detail_attrs = { error_message: error_message.to_s.truncate(65535) }
967
+ if execution.detail
968
+ execution.detail.update_columns(detail_attrs)
969
+ else
970
+ RubyLLM::Agents::ExecutionDetail.create!(detail_attrs.merge(execution_id: execution.id))
971
+ end
972
+ rescue StandardError
973
+ # Non-critical — error_class on execution is sufficient for filtering
974
+ end
928
975
  rescue StandardError => e
929
976
  Rails.logger.error("[RubyLLM::Agents] CRITICAL: Failed emergency status update for execution #{execution&.id}: #{e.message}")
930
977
  end
@@ -68,13 +68,7 @@ module RubyLLM
68
68
  extend ActiveSupport::Concern
69
69
 
70
70
  included do
71
- # Executions tracked for this tenant
72
- has_many :llm_executions,
73
- class_name: "RubyLLM::Agents::Execution",
74
- as: :tenant_record,
75
- dependent: :nullify
76
-
77
- # Link to gem's Tenant model (new name)
71
+ # Link to gem's Tenant model via polymorphic association
78
72
  has_one :llm_tenant_record,
79
73
  class_name: "RubyLLM::Agents::Tenant",
80
74
  as: :tenant_record,
@@ -110,6 +104,13 @@ module RubyLLM
110
104
  api_keys: api_keys
111
105
  }
112
106
 
107
+ # Executions tracked for this tenant via tenant_id string column
108
+ has_many :llm_executions,
109
+ class_name: "RubyLLM::Agents::Execution",
110
+ foreign_key: :tenant_id,
111
+ primary_key: id,
112
+ dependent: :nullify
113
+
113
114
  # Auto-create tenant record callback
114
115
  after_create :create_default_llm_tenant if llm_tenant_options[:budget]
115
116
  end
@@ -4,6 +4,6 @@ module RubyLLM
4
4
  module Agents
5
5
  # Current version of the RubyLLM::Agents gem
6
6
  # @return [String] Semantic version string
7
- VERSION = "1.3.3"
7
+ VERSION = "2.0.0"
8
8
  end
9
9
  end