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
@@ -1,156 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module Agents
5
- class Workflow
6
- # Fiber-based concurrent executor for parallel workflows
7
- #
8
- # Provides an alternative to ThreadPool that uses Ruby's Fiber scheduler
9
- # for lightweight concurrency. Automatically used when the async gem is
10
- # available and we're inside an async context.
11
- #
12
- # @example Basic usage
13
- # executor = AsyncExecutor.new(max_concurrent: 4)
14
- # executor.post { perform_task_1 }
15
- # executor.post { perform_task_2 }
16
- # executor.wait_for_completion
17
- #
18
- # @example With fail-fast
19
- # executor = AsyncExecutor.new(max_concurrent: 4)
20
- # executor.post { risky_task }
21
- # executor.abort! if something_failed
22
- # executor.wait_for_completion
23
- #
24
- # @api private
25
- class AsyncExecutor
26
- attr_reader :max_concurrent
27
-
28
- # Creates a new async executor
29
- #
30
- # @param max_concurrent [Integer] Maximum concurrent fibers (default: 10)
31
- def initialize(max_concurrent: 10)
32
- @max_concurrent = max_concurrent
33
- @tasks = []
34
- @results = []
35
- @mutex = Mutex.new
36
- @aborted = false
37
- @semaphore = nil
38
- end
39
-
40
- # Submits a task for execution
41
- #
42
- # @yield Block to execute
43
- # @return [void]
44
- def post(&block)
45
- @mutex.synchronize do
46
- @tasks << block
47
- end
48
- end
49
-
50
- # Signals that remaining tasks should be skipped
51
- #
52
- # Currently running tasks will complete, but pending tasks will be skipped.
53
- #
54
- # @return [void]
55
- def abort!
56
- @mutex.synchronize do
57
- @aborted = true
58
- end
59
- end
60
-
61
- # Returns whether the executor has been aborted
62
- #
63
- # @return [Boolean] true if abort! was called
64
- def aborted?
65
- @mutex.synchronize { @aborted }
66
- end
67
-
68
- # Executes all submitted tasks and waits for completion
69
- #
70
- # @param timeout [Integer, nil] Maximum seconds to wait (nil = indefinite)
71
- # @return [Boolean] true if all tasks completed, false if timeout
72
- def wait_for_completion(timeout: nil)
73
- return true if @tasks.empty?
74
-
75
- ensure_async_available!
76
-
77
- @semaphore = ::Async::Semaphore.new(@max_concurrent)
78
-
79
- if timeout
80
- execute_with_timeout(timeout)
81
- else
82
- execute_all
83
- true
84
- end
85
- end
86
-
87
- # Shuts down the executor
88
- #
89
- # For AsyncExecutor this is a no-op since fibers are garbage collected.
90
- #
91
- # @param timeout [Integer] Ignored for async executor
92
- # @return [void]
93
- def shutdown(timeout: 5)
94
- # No-op for fiber-based executor
95
- # Fibers are lightweight and garbage collected
96
- end
97
-
98
- # Waits for termination (compatibility with ThreadPool)
99
- #
100
- # @param timeout [Integer] Ignored for async executor
101
- # @return [void]
102
- def wait_for_termination(timeout: 5)
103
- # No-op for fiber-based executor
104
- end
105
-
106
- private
107
-
108
- # Executes all tasks with async
109
- #
110
- # @return [void]
111
- def execute_all
112
- Kernel.send(:Async) do
113
- @tasks.map do |task|
114
- Kernel.send(:Async) do
115
- next if aborted?
116
-
117
- @semaphore.acquire do
118
- next if aborted?
119
- task.call
120
- end
121
- end
122
- end.map(&:wait)
123
- end.wait
124
- end
125
-
126
- # Executes all tasks with a timeout
127
- #
128
- # @param timeout [Integer] Maximum seconds to wait
129
- # @return [Boolean] true if completed, false if timeout
130
- def execute_with_timeout(timeout)
131
- completed = false
132
-
133
- Kernel.send(:Async) do |task|
134
- task.with_timeout(timeout) do
135
- execute_all
136
- completed = true
137
- rescue ::Async::TimeoutError
138
- completed = false
139
- end
140
- end.wait
141
-
142
- completed
143
- end
144
-
145
- # Ensures async gem is available
146
- #
147
- # @raise [RuntimeError] If async gem is not loaded
148
- def ensure_async_available!
149
- return if defined?(::Async) && defined?(::Async::Semaphore)
150
-
151
- raise "AsyncExecutor requires the 'async' gem. Add gem 'async' to your Gemfile."
152
- end
153
- end
154
- end
155
- end
156
- end
@@ -1,467 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../wait_result"
4
- require_relative "../throttle_manager"
5
- require_relative "../approval"
6
- require_relative "../approval_store"
7
- require_relative "../notifiers"
8
-
9
- module RubyLLM
10
- module Agents
11
- class Workflow
12
- module DSL
13
- # Main executor for workflows using the refined DSL
14
- #
15
- # Handles the execution of steps in order, including sequential
16
- # steps and parallel groups, with full support for routing,
17
- # conditions, retries, and error handling.
18
- #
19
- # @api private
20
- class Executor
21
- attr_reader :workflow, :results, :errors, :status
22
-
23
- # @param workflow [Workflow] The workflow instance
24
- def initialize(workflow)
25
- @workflow = workflow
26
- @results = {}
27
- @errors = {}
28
- @status = "success"
29
- @halted = false
30
- @skip_next_step = false
31
- @throttle_manager = ThrottleManager.new
32
- @wait_results = {}
33
- end
34
-
35
- # Executes all workflow steps
36
- #
37
- # @yield [chunk] Streaming callback
38
- # @return [Workflow::Result] The workflow result
39
- def execute(&block)
40
- @workflow_started_at = Time.current
41
-
42
- # Validate input schema before execution
43
- validate_input!
44
-
45
- run_hooks(:before_workflow)
46
-
47
- catch(:halt_workflow) do
48
- execute_steps(&block)
49
- end
50
-
51
- run_hooks(:after_workflow)
52
-
53
- build_result
54
- rescue InputSchema::ValidationError
55
- # Re-raise validation errors - these should not be caught
56
- raise
57
- rescue StandardError => e
58
- @status = "error"
59
- @errors[:workflow] = e
60
- build_result(error: e)
61
- end
62
-
63
- private
64
-
65
- # Validates input against the schema if defined
66
- #
67
- # This is called at the start of execution to fail fast on invalid input.
68
- # Also populates the validated_input for later access.
69
- #
70
- # @raise [InputSchema::ValidationError] If input validation fails
71
- def validate_input!
72
- schema = workflow.class.input_schema
73
- return unless schema
74
-
75
- # This will raise ValidationError if input is invalid
76
- validated = schema.validate!(workflow.options)
77
- workflow.instance_variable_set(:@validated_input, OpenStruct.new(validated))
78
- end
79
-
80
- def execute_steps(&block)
81
- previous_result = nil
82
-
83
- workflow.class.step_order.each do |item|
84
- break if @halted
85
-
86
- # Handle skip_next from wait timeout
87
- if @skip_next_step
88
- @skip_next_step = false
89
- next if item.is_a?(Symbol)
90
- end
91
-
92
- case item
93
- when Symbol
94
- previous_result = execute_single_step(item, previous_result, &block)
95
- when ParallelGroup
96
- previous_result = execute_parallel_group(item, &block)
97
- when WaitConfig
98
- wait_result = execute_wait_step(item)
99
- @wait_results[item.object_id] = wait_result
100
- handle_wait_result(wait_result)
101
- end
102
- end
103
- end
104
-
105
- def execute_single_step(step_name, previous_result, &block)
106
- config = workflow.class.step_configs[step_name]
107
- return previous_result unless config
108
-
109
- # Apply throttling if configured
110
- apply_throttle(step_name, config)
111
-
112
- run_hooks(:before_step, step_name, workflow.step_results)
113
- run_hooks(:on_step_start, step_name, config.resolve_input(workflow, previous_result))
114
-
115
- started_at = Time.current
116
-
117
- result = catch(:skip_step) do
118
- executor = StepExecutor.new(workflow, config)
119
- executor.execute(previous_result, &block)
120
- end
121
-
122
- # Handle skip_step catch
123
- if result.is_a?(Hash) && result[:skipped]
124
- result = if result[:default]
125
- SimpleResult.new(content: result[:default], success: true)
126
- else
127
- SkippedResult.new(step_name, reason: result[:reason])
128
- end
129
- end
130
-
131
- duration_ms = ((Time.current - started_at) * 1000).round
132
-
133
- @results[step_name] = result
134
- workflow.instance_variable_get(:@step_results)[step_name] = result
135
-
136
- # Update status based on result
137
- update_status_from_result(step_name, result, config)
138
-
139
- run_hooks(:after_step, step_name, result, duration_ms)
140
- run_hooks(:on_step_complete, step_name, result, duration_ms)
141
-
142
- # Return nil on error for critical steps to prevent passing bad data
143
- if result.respond_to?(:error?) && result.error? && config.critical?
144
- @halted = true
145
- return nil
146
- end
147
-
148
- result
149
- rescue StandardError => e
150
- handle_step_error(step_name, e, config)
151
- end
152
-
153
- def execute_parallel_group(group, &block)
154
- results_mutex = Mutex.new
155
- group_results = {}
156
- group_errors = {}
157
-
158
- # Determine pool size
159
- pool_size = group.concurrency || group.step_names.size
160
- pool = create_executor_pool(pool_size)
161
-
162
- # Get the last result before this parallel group for input
163
- last_sequential_step = workflow.class.step_order
164
- .take_while { |item| item != group }
165
- .select { |item| item.is_a?(Symbol) }
166
- .last
167
- previous_result = last_sequential_step ? @results[last_sequential_step] : nil
168
-
169
- group.step_names.each do |step_name|
170
- pool.post do
171
- Thread.current.name = "parallel-#{step_name}"
172
-
173
- begin
174
- config = workflow.class.step_configs[step_name]
175
- next unless config
176
-
177
- executor = StepExecutor.new(workflow, config)
178
- result = executor.execute(previous_result, &block)
179
-
180
- results_mutex.synchronize do
181
- group_results[step_name] = result
182
- @results[step_name] = result
183
- workflow.instance_variable_get(:@step_results)[step_name] = result
184
-
185
- # Fail-fast handling
186
- if group.fail_fast? && result.respond_to?(:error?) && result.error? && config.critical?
187
- pool.abort! if pool.respond_to?(:abort!)
188
- end
189
- end
190
- rescue StandardError => e
191
- results_mutex.synchronize do
192
- group_errors[step_name] = e
193
- @errors[step_name] = e
194
-
195
- if group.fail_fast?
196
- pool.abort! if pool.respond_to?(:abort!)
197
- end
198
- end
199
- end
200
- end
201
- end
202
-
203
- pool.wait_for_completion
204
- pool.shutdown
205
-
206
- # Update overall status
207
- update_parallel_status(group, group_results, group_errors)
208
-
209
- # Return combined results as a hash-like object
210
- ParallelGroupResult.new(group.name, group_results)
211
- end
212
-
213
- def create_executor_pool(size)
214
- config = RubyLLM::Agents.configuration
215
-
216
- if config.respond_to?(:async_context?) && config.async_context?
217
- AsyncExecutor.new(max_concurrent: size)
218
- else
219
- ThreadPool.new(size: size)
220
- end
221
- end
222
-
223
- # Executes a wait step
224
- #
225
- # @param wait_config [WaitConfig] The wait configuration
226
- # @return [WaitResult] The wait result
227
- def execute_wait_step(wait_config)
228
- executor = WaitExecutor.new(wait_config, workflow)
229
- executor.execute
230
- rescue StandardError => e
231
- # Return a failed result instead of crashing
232
- Workflow::WaitResult.timeout(
233
- wait_config.type,
234
- 0,
235
- :fail,
236
- error: "#{e.class}: #{e.message}"
237
- )
238
- end
239
-
240
- # Handles the result of a wait step
241
- #
242
- # @param wait_result [WaitResult] The wait result
243
- # @return [void]
244
- def handle_wait_result(wait_result)
245
- if wait_result.timeout? && wait_result.timeout_action == :fail
246
- @status = "error"
247
- @halted = true
248
- @errors[:wait] = "Wait timed out: #{wait_result.type}"
249
- elsif wait_result.rejected?
250
- @status = "error"
251
- @halted = true
252
- @errors[:wait] = "Approval rejected: #{wait_result.rejection_reason}"
253
- elsif wait_result.should_skip_next?
254
- @skip_next_step = true
255
- end
256
- end
257
-
258
- # Applies throttling for a step if configured
259
- #
260
- # @param step_name [Symbol] The step name
261
- # @param config [StepConfig] The step configuration
262
- # @return [void]
263
- def apply_throttle(step_name, config)
264
- return unless config.throttled?
265
-
266
- if config.throttle
267
- @throttle_manager.throttle("step:#{step_name}", config.throttle)
268
- elsif config.rate_limit
269
- @throttle_manager.rate_limit(
270
- "step:#{step_name}",
271
- calls: config.rate_limit[:calls],
272
- per: config.rate_limit[:per]
273
- )
274
- end
275
- end
276
-
277
- def handle_step_error(step_name, error, config)
278
- @errors[step_name] = error
279
-
280
- run_hooks(:on_step_error, step_name, error)
281
- run_hooks(:on_step_failure, step_name, error, workflow.step_results)
282
-
283
- # Build error result
284
- error_result = Pipeline::ErrorResult.new(
285
- step_name: step_name,
286
- error_class: error.class.name,
287
- error_message: error.message
288
- )
289
-
290
- @results[step_name] = error_result
291
- workflow.instance_variable_get(:@step_results)[step_name] = error_result
292
-
293
- if config.optional?
294
- @status = "partial" if @status == "success"
295
- config.default_value ? SimpleResult.new(content: config.default_value, success: true) : nil
296
- else
297
- @status = "error"
298
- @halted = true
299
- nil
300
- end
301
- end
302
-
303
- def update_status_from_result(step_name, result, config)
304
- return unless result.respond_to?(:error?) && result.error?
305
-
306
- if config.optional?
307
- @status = "partial" if @status == "success"
308
- else
309
- @status = "error"
310
- end
311
- end
312
-
313
- def update_parallel_status(group, group_results, group_errors)
314
- # Check for errors
315
- group.step_names.each do |step_name|
316
- config = workflow.class.step_configs[step_name]
317
-
318
- if group_errors[step_name]
319
- if config&.optional?
320
- @status = "partial" if @status == "success"
321
- else
322
- @status = "error"
323
- end
324
- elsif group_results[step_name]&.respond_to?(:error?) && group_results[step_name].error?
325
- if config&.optional?
326
- @status = "partial" if @status == "success"
327
- else
328
- @status = "error"
329
- end
330
- end
331
- end
332
- end
333
-
334
- def run_hooks(hook_name, *args)
335
- workflow.send(:run_hooks, hook_name, *args)
336
- end
337
-
338
- def build_result(error: nil)
339
- # Get final content from last successful step
340
- final_content = extract_final_content
341
-
342
- # Validate output if schema defined
343
- if workflow.class.output_schema && final_content
344
- begin
345
- workflow.class.output_schema.validate!(final_content)
346
- rescue InputSchema::ValidationError => e
347
- @errors[:output_validation] = e
348
- @status = "error" if @status == "success"
349
- end
350
- end
351
-
352
- Workflow::Result.new(
353
- content: final_content,
354
- workflow_type: workflow.class.name,
355
- workflow_id: workflow.workflow_id,
356
- steps: @results,
357
- errors: @errors,
358
- status: @status,
359
- error_class: error&.class&.name,
360
- error_message: error&.message,
361
- started_at: @workflow_started_at,
362
- completed_at: Time.current,
363
- duration_ms: (((Time.current - @workflow_started_at) * 1000).round if @workflow_started_at)
364
- )
365
- end
366
-
367
- def extract_final_content
368
- # Find the last successful result
369
- workflow.class.step_order.reverse.each do |item|
370
- case item
371
- when Symbol
372
- result = @results[item]
373
- next if result.nil?
374
- next if result.respond_to?(:skipped?) && result.skipped?
375
- next if result.respond_to?(:error?) && result.error?
376
- return result.content if result.respond_to?(:content)
377
- when ParallelGroup
378
- # For parallel groups, return the combined content
379
- group_content = {}
380
- item.step_names.each do |step_name|
381
- result = @results[step_name]
382
- next if result.nil? || (result.respond_to?(:error?) && result.error?)
383
- group_content[step_name] = result.respond_to?(:content) ? result.content : result
384
- end
385
- return group_content if group_content.any?
386
- when WaitConfig
387
- # Wait steps don't contribute content, skip them
388
- next
389
- end
390
- end
391
-
392
- nil
393
- end
394
- end
395
-
396
- # Result wrapper for parallel group execution
397
- #
398
- # Provides access to individual step results within a parallel group.
399
- #
400
- # @api private
401
- class ParallelGroupResult
402
- attr_reader :name, :results
403
-
404
- def initialize(name, results)
405
- @name = name
406
- @results = results
407
- end
408
-
409
- def content
410
- @results.transform_values { |r| r&.content }
411
- end
412
-
413
- def [](key)
414
- @results[key]
415
- end
416
-
417
- def success?
418
- @results.values.all? { |r| r.nil? || !r.respond_to?(:error?) || !r.error? }
419
- end
420
-
421
- def error?
422
- !success?
423
- end
424
-
425
- def to_h
426
- content
427
- end
428
-
429
- def method_missing(name, *args, &block)
430
- if @results.key?(name)
431
- @results[name]
432
- elsif content.key?(name)
433
- content[name]
434
- else
435
- super
436
- end
437
- end
438
-
439
- def respond_to_missing?(name, include_private = false)
440
- @results.key?(name) || content.key?(name) || super
441
- end
442
-
443
- # Token/cost aggregation
444
- def input_tokens
445
- @results.values.compact.sum { |r| r.respond_to?(:input_tokens) ? r.input_tokens : 0 }
446
- end
447
-
448
- def output_tokens
449
- @results.values.compact.sum { |r| r.respond_to?(:output_tokens) ? r.output_tokens : 0 }
450
- end
451
-
452
- def total_tokens
453
- input_tokens + output_tokens
454
- end
455
-
456
- def cached_tokens
457
- @results.values.compact.sum { |r| r.respond_to?(:cached_tokens) ? r.cached_tokens : 0 }
458
- end
459
-
460
- def total_cost
461
- @results.values.compact.sum { |r| r.respond_to?(:total_cost) ? r.total_cost : 0.0 }
462
- end
463
- end
464
- end
465
- end
466
- end
467
- end