ruby_llm-agents 1.0.0 → 1.2.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.
- checksums.yaml +4 -4
- data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +9 -3
- data/app/controllers/concerns/ruby_llm/agents/sortable.rb +58 -0
- data/app/controllers/ruby_llm/agents/agents_controller.rb +59 -16
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +144 -20
- data/app/controllers/ruby_llm/agents/executions_controller.rb +13 -16
- data/app/controllers/ruby_llm/agents/workflows_controller.rb +279 -90
- data/app/helpers/ruby_llm/agents/application_helper.rb +100 -0
- data/app/mailers/ruby_llm/agents/alert_mailer.rb +84 -0
- data/app/mailers/ruby_llm/agents/application_mailer.rb +28 -0
- data/app/models/ruby_llm/agents/execution/analytics.rb +170 -20
- data/app/models/ruby_llm/agents/execution/scopes.rb +0 -31
- data/app/models/ruby_llm/agents/execution/workflow.rb +0 -129
- data/app/models/ruby_llm/agents/execution.rb +50 -14
- data/app/models/ruby_llm/agents/tenant/budgetable.rb +277 -0
- data/app/models/ruby_llm/agents/tenant/configurable.rb +135 -0
- data/app/models/ruby_llm/agents/tenant/trackable.rb +310 -0
- data/app/models/ruby_llm/agents/tenant.rb +146 -0
- data/app/models/ruby_llm/agents/tenant_budget.rb +12 -253
- data/app/services/ruby_llm/agents/agent_registry.rb +18 -12
- data/app/views/layouts/ruby_llm/agents/application.html.erb +72 -76
- data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -12
- data/app/views/ruby_llm/agents/agents/_sortable_header.html.erb +56 -0
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +5 -15
- data/app/views/ruby_llm/agents/agents/index.html.erb +271 -100
- data/app/views/ruby_llm/agents/agents/show.html.erb +1 -0
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +107 -0
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +18 -0
- data/app/views/ruby_llm/agents/api_configurations/show.html.erb +4 -1
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +66 -359
- data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +56 -0
- data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +115 -0
- data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +35 -60
- data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +17 -6
- data/app/views/ruby_llm/agents/dashboard/index.html.erb +373 -72
- data/app/views/ruby_llm/agents/executions/_execution.html.erb +0 -1
- data/app/views/ruby_llm/agents/executions/_filters.html.erb +51 -39
- data/app/views/ruby_llm/agents/executions/_list.html.erb +53 -195
- data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +5 -20
- data/app/views/ruby_llm/agents/executions/index.html.erb +7 -83
- data/app/views/ruby_llm/agents/executions/show.html.erb +10 -20
- data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +2 -1
- data/app/views/ruby_llm/agents/shared/_doc_link.html.erb +12 -0
- data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +3 -15
- data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_sortable_header.html.erb +53 -0
- data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +7 -0
- data/app/views/ruby_llm/agents/shared/_status_dot.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +9 -35
- data/app/views/ruby_llm/agents/system_config/show.html.erb +4 -1
- data/app/views/ruby_llm/agents/tenants/index.html.erb +4 -1
- data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +7 -15
- data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +539 -0
- data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +920 -0
- data/app/views/ruby_llm/agents/workflows/index.html.erb +179 -0
- data/app/views/ruby_llm/agents/workflows/show.html.erb +164 -139
- data/config/routes.rb +1 -1
- data/lib/generators/ruby_llm_agents/agent_generator.rb +6 -36
- data/lib/generators/ruby_llm_agents/background_remover_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/embedder_generator.rb +5 -38
- data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/image_editor_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/image_generator_generator.rb +8 -41
- data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +18 -46
- data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/image_variator_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/install_generator.rb +33 -56
- data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +480 -0
- data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +42 -22
- data/lib/generators/ruby_llm_agents/restructure_generator.rb +2 -2
- data/lib/generators/ruby_llm_agents/speaker_generator.rb +8 -39
- data/lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt +13 -2
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +5 -8
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +40 -42
- data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +20 -22
- data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +24 -26
- data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +20 -22
- data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +19 -17
- data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +31 -33
- data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +125 -127
- data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +20 -18
- data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +19 -17
- data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +19 -17
- data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +38 -40
- data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +42 -44
- data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +48 -0
- data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +19 -21
- data/lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt +11 -0
- data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +72 -0
- data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +19 -21
- data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +20 -22
- data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +15 -17
- data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +25 -27
- data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +19 -21
- data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +20 -22
- data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +17 -19
- data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +15 -17
- data/lib/generators/ruby_llm_agents/templates/rename_tenant_budgets_to_tenants_migration.rb.tt +34 -0
- data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +87 -24
- data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +21 -27
- data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +46 -54
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +31 -39
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +22 -28
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +53 -63
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +46 -56
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +23 -31
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +22 -30
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +23 -31
- data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +38 -46
- data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +7 -7
- data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +59 -71
- data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +274 -23
- data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +29 -31
- data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +28 -30
- data/lib/generators/ruby_llm_agents/transcriber_generator.rb +10 -43
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +26 -0
- data/lib/ruby_llm/agents/core/configuration.rb +55 -43
- data/lib/ruby_llm/agents/core/llm_tenant.rb +60 -60
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +26 -0
- data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +4 -2
- data/lib/ruby_llm/agents/pipeline.rb +69 -0
- data/lib/ruby_llm/agents/workflow/approval.rb +205 -0
- data/lib/ruby_llm/agents/workflow/approval_store.rb +179 -0
- data/lib/ruby_llm/agents/workflow/dsl/executor.rb +467 -0
- data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +244 -0
- data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +289 -0
- data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +107 -0
- data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +150 -0
- data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +187 -0
- data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +352 -0
- data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +415 -0
- data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +257 -0
- data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +317 -0
- data/lib/ruby_llm/agents/workflow/dsl.rb +576 -0
- data/lib/ruby_llm/agents/workflow/instrumentation.rb +2 -7
- data/lib/ruby_llm/agents/workflow/notifiers/base.rb +117 -0
- data/lib/ruby_llm/agents/workflow/notifiers/email.rb +117 -0
- data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +180 -0
- data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +121 -0
- data/lib/ruby_llm/agents/workflow/notifiers.rb +70 -0
- data/lib/ruby_llm/agents/workflow/orchestrator.rb +190 -23
- data/lib/ruby_llm/agents/workflow/result.rb +202 -0
- data/lib/ruby_llm/agents/workflow/throttle_manager.rb +206 -0
- data/lib/ruby_llm/agents/workflow/wait_result.rb +213 -0
- metadata +43 -6
- data/app/views/ruby_llm/agents/dashboard/_execution_item.html.erb +0 -66
- data/lib/ruby_llm/agents/workflow/parallel.rb +0 -299
- data/lib/ruby_llm/agents/workflow/pipeline.rb +0 -306
- data/lib/ruby_llm/agents/workflow/router.rb +0 -429
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
class Workflow
|
|
6
|
+
module DSL
|
|
7
|
+
# Executes wait steps within a workflow
|
|
8
|
+
#
|
|
9
|
+
# Handles the four types of waits:
|
|
10
|
+
# - delay: Simple time-based pause
|
|
11
|
+
# - until: Poll until a condition is met
|
|
12
|
+
# - schedule: Wait until a specific time
|
|
13
|
+
# - approval: Wait for human approval
|
|
14
|
+
#
|
|
15
|
+
# @example Executing a delay wait
|
|
16
|
+
# executor = WaitExecutor.new(wait_config, workflow)
|
|
17
|
+
# result = executor.execute
|
|
18
|
+
#
|
|
19
|
+
# @api private
|
|
20
|
+
class WaitExecutor
|
|
21
|
+
# @param config [WaitConfig] The wait configuration
|
|
22
|
+
# @param workflow [Workflow] The workflow instance
|
|
23
|
+
# @param approval_store [ApprovalStore, nil] Custom approval store
|
|
24
|
+
def initialize(config, workflow, approval_store: nil)
|
|
25
|
+
@config = config
|
|
26
|
+
@workflow = workflow
|
|
27
|
+
@approval_store = approval_store || ApprovalStore.store
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Execute the wait step
|
|
31
|
+
#
|
|
32
|
+
# @return [WaitResult] The result of the wait
|
|
33
|
+
def execute
|
|
34
|
+
# Check conditions first
|
|
35
|
+
unless @config.should_execute?(@workflow)
|
|
36
|
+
return WaitResult.skipped(@config.type, reason: "Condition not met")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
case @config.type
|
|
40
|
+
when :delay
|
|
41
|
+
execute_delay
|
|
42
|
+
when :until
|
|
43
|
+
execute_until
|
|
44
|
+
when :schedule
|
|
45
|
+
execute_schedule
|
|
46
|
+
when :approval
|
|
47
|
+
execute_approval
|
|
48
|
+
else
|
|
49
|
+
raise ArgumentError, "Unknown wait type: #{@config.type}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Execute a simple delay
|
|
56
|
+
#
|
|
57
|
+
# @return [WaitResult]
|
|
58
|
+
def execute_delay
|
|
59
|
+
duration = resolve_duration(@config.duration)
|
|
60
|
+
sleep_with_interruption(duration)
|
|
61
|
+
WaitResult.success(:delay, duration)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Execute a conditional wait (polling)
|
|
65
|
+
#
|
|
66
|
+
# @return [WaitResult]
|
|
67
|
+
def execute_until
|
|
68
|
+
started_at = Time.now
|
|
69
|
+
interval = normalize_duration(@config.poll_interval)
|
|
70
|
+
timeout = @config.timeout ? normalize_duration(@config.timeout) : nil
|
|
71
|
+
max_interval = @config.max_interval ? normalize_duration(@config.max_interval) : nil
|
|
72
|
+
|
|
73
|
+
loop do
|
|
74
|
+
# Check condition
|
|
75
|
+
if evaluate_condition(@config.condition)
|
|
76
|
+
waited = Time.now - started_at
|
|
77
|
+
return WaitResult.success(:until, waited)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Check timeout
|
|
81
|
+
if timeout
|
|
82
|
+
elapsed = Time.now - started_at
|
|
83
|
+
if elapsed >= timeout
|
|
84
|
+
return handle_timeout(:until, elapsed)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Wait before next poll
|
|
89
|
+
sleep_with_interruption(interval)
|
|
90
|
+
|
|
91
|
+
# Apply exponential backoff if configured
|
|
92
|
+
if @config.exponential_backoff?
|
|
93
|
+
interval = apply_backoff(interval, max_interval)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Execute a scheduled wait
|
|
99
|
+
#
|
|
100
|
+
# @return [WaitResult]
|
|
101
|
+
def execute_schedule
|
|
102
|
+
target_time = evaluate_time(@config.condition)
|
|
103
|
+
|
|
104
|
+
unless target_time.is_a?(Time)
|
|
105
|
+
raise ArgumentError, "Schedule condition must return a Time, got #{target_time.class}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
wait_duration = target_time - Time.now
|
|
109
|
+
|
|
110
|
+
if wait_duration > 0
|
|
111
|
+
sleep_with_interruption(wait_duration)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
WaitResult.success(:schedule, [wait_duration, 0].max, target_time: target_time)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Execute a human approval wait
|
|
118
|
+
#
|
|
119
|
+
# @return [WaitResult]
|
|
120
|
+
def execute_approval
|
|
121
|
+
started_at = Time.now
|
|
122
|
+
|
|
123
|
+
# Create approval request
|
|
124
|
+
approval = create_approval_request
|
|
125
|
+
|
|
126
|
+
# Save to store
|
|
127
|
+
@approval_store.save(approval)
|
|
128
|
+
|
|
129
|
+
# Send notifications
|
|
130
|
+
send_notifications(approval)
|
|
131
|
+
|
|
132
|
+
# Set up reminder tracking
|
|
133
|
+
reminder_sent = false
|
|
134
|
+
reminder_after = @config.reminder_after ? normalize_duration(@config.reminder_after) : nil
|
|
135
|
+
reminder_interval = @config.reminder_interval ? normalize_duration(@config.reminder_interval) : nil
|
|
136
|
+
|
|
137
|
+
# Poll for approval or timeout
|
|
138
|
+
timeout = @config.timeout ? normalize_duration(@config.timeout) : nil
|
|
139
|
+
poll_interval = normalize_duration(@config.poll_interval)
|
|
140
|
+
|
|
141
|
+
loop do
|
|
142
|
+
# Refresh approval from store
|
|
143
|
+
approval = @approval_store.find(approval.id)
|
|
144
|
+
|
|
145
|
+
unless approval
|
|
146
|
+
waited = Time.now - started_at
|
|
147
|
+
return WaitResult.timeout(:approval, waited, :fail,
|
|
148
|
+
error: "Approval not found")
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Check if approved
|
|
152
|
+
if approval.approved?
|
|
153
|
+
waited = Time.now - started_at
|
|
154
|
+
return WaitResult.approved(approval.id, approval.approved_by, waited)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Check if rejected
|
|
158
|
+
if approval.rejected?
|
|
159
|
+
waited = Time.now - started_at
|
|
160
|
+
return WaitResult.rejected(approval.id, approval.rejected_by, waited,
|
|
161
|
+
reason: approval.reason)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Check if expired
|
|
165
|
+
if approval.expired? || approval.timed_out?
|
|
166
|
+
waited = Time.now - started_at
|
|
167
|
+
return handle_timeout(:approval, waited, approval_id: approval.id)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Check timeout
|
|
171
|
+
if timeout
|
|
172
|
+
elapsed = Time.now - started_at
|
|
173
|
+
if elapsed >= timeout
|
|
174
|
+
approval.expire!
|
|
175
|
+
@approval_store.save(approval)
|
|
176
|
+
return handle_timeout(:approval, elapsed, approval_id: approval.id)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Check if reminder should be sent
|
|
181
|
+
if reminder_after && approval.should_remind?(reminder_after,
|
|
182
|
+
reminder_interval: reminder_interval)
|
|
183
|
+
send_reminder(approval)
|
|
184
|
+
approval.mark_reminded!
|
|
185
|
+
@approval_store.save(approval)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Wait before next poll
|
|
189
|
+
sleep_with_interruption(poll_interval)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def create_approval_request
|
|
194
|
+
Approval.new(
|
|
195
|
+
workflow_id: @workflow.object_id.to_s,
|
|
196
|
+
workflow_type: @workflow.class.name,
|
|
197
|
+
name: @config.name,
|
|
198
|
+
approvers: @config.approvers,
|
|
199
|
+
expires_at: @config.timeout ? Time.now + normalize_duration(@config.timeout) : nil,
|
|
200
|
+
metadata: {
|
|
201
|
+
workflow_input: @workflow.input.to_h,
|
|
202
|
+
created_by: "workflow"
|
|
203
|
+
}
|
|
204
|
+
)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def send_notifications(approval)
|
|
208
|
+
return if @config.notify_channels.empty?
|
|
209
|
+
|
|
210
|
+
message = resolve_message(@config.message, approval)
|
|
211
|
+
Notifiers.notify(approval, message, channels: @config.notify_channels)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def send_reminder(approval)
|
|
215
|
+
return if @config.notify_channels.empty?
|
|
216
|
+
|
|
217
|
+
message = resolve_message(@config.message, approval)
|
|
218
|
+
@config.notify_channels.each do |channel|
|
|
219
|
+
notifier = Notifiers[channel]
|
|
220
|
+
notifier&.remind(approval, message)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def resolve_message(message_config, approval)
|
|
225
|
+
case message_config
|
|
226
|
+
when String
|
|
227
|
+
message_config
|
|
228
|
+
when Proc
|
|
229
|
+
@workflow.instance_exec(approval, &message_config)
|
|
230
|
+
else
|
|
231
|
+
"Approval required: #{approval.name}"
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def handle_timeout(type, elapsed, **metadata)
|
|
236
|
+
action = @config.on_timeout
|
|
237
|
+
|
|
238
|
+
case action
|
|
239
|
+
when :continue
|
|
240
|
+
WaitResult.timeout(type, elapsed, :continue, **metadata)
|
|
241
|
+
when :skip_next
|
|
242
|
+
WaitResult.timeout(type, elapsed, :skip_next, **metadata)
|
|
243
|
+
when :escalate
|
|
244
|
+
handle_escalation(type, elapsed, metadata)
|
|
245
|
+
else # :fail
|
|
246
|
+
WaitResult.timeout(type, elapsed, :fail, **metadata)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def handle_escalation(type, elapsed, metadata)
|
|
251
|
+
if @config.escalate_to
|
|
252
|
+
# Create escalated approval or notify escalation target
|
|
253
|
+
WaitResult.timeout(type, elapsed, :escalate,
|
|
254
|
+
escalated_to: @config.escalate_to,
|
|
255
|
+
**metadata)
|
|
256
|
+
else
|
|
257
|
+
WaitResult.timeout(type, elapsed, :fail, **metadata)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def resolve_duration(duration)
|
|
262
|
+
normalize_duration(duration)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def normalize_duration(duration)
|
|
266
|
+
if duration.respond_to?(:to_f)
|
|
267
|
+
duration.to_f
|
|
268
|
+
else
|
|
269
|
+
duration.to_i.to_f
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def evaluate_condition(condition)
|
|
274
|
+
case condition
|
|
275
|
+
when Proc
|
|
276
|
+
@workflow.instance_exec(&condition)
|
|
277
|
+
when Symbol
|
|
278
|
+
@workflow.send(condition)
|
|
279
|
+
else
|
|
280
|
+
!!condition
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def evaluate_time(time_config)
|
|
285
|
+
case time_config
|
|
286
|
+
when Proc
|
|
287
|
+
@workflow.instance_exec(&time_config)
|
|
288
|
+
when Time
|
|
289
|
+
time_config
|
|
290
|
+
else
|
|
291
|
+
raise ArgumentError, "Schedule time must be a Time or Proc, got #{time_config.class}"
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def apply_backoff(current_interval, max_interval)
|
|
296
|
+
new_interval = current_interval * @config.backoff
|
|
297
|
+
|
|
298
|
+
if max_interval
|
|
299
|
+
[new_interval, max_interval].min
|
|
300
|
+
else
|
|
301
|
+
new_interval
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def sleep_with_interruption(duration)
|
|
306
|
+
# Use Async.sleep if available, otherwise Kernel.sleep
|
|
307
|
+
if defined?(::Async::Task) && ::Async::Task.current?
|
|
308
|
+
::Async::Task.current.sleep(duration)
|
|
309
|
+
else
|
|
310
|
+
Kernel.sleep(duration)
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|