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,185 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module Agents
5
- class Workflow
6
- # Simple bounded thread pool for parallel workflow execution
7
- #
8
- # Provides a fixed-size pool of worker threads that process submitted tasks.
9
- # Supports fail-fast abort and graceful shutdown.
10
- #
11
- # @example Basic usage
12
- # pool = ThreadPool.new(size: 4)
13
- # pool.post { perform_task_1 }
14
- # pool.post { perform_task_2 }
15
- # pool.wait_for_completion
16
- # pool.shutdown
17
- #
18
- # @example With fail-fast
19
- # pool = ThreadPool.new(size: 4)
20
- # begin
21
- # pool.post { risky_task }
22
- # rescue => e
23
- # pool.abort! # Signal workers to stop
24
- # end
25
- # pool.shutdown
26
- #
27
- # @api private
28
- class ThreadPool
29
- attr_reader :size
30
-
31
- # Creates a new thread pool
32
- #
33
- # @param size [Integer] Number of worker threads (default: 4)
34
- def initialize(size: 4)
35
- @size = size
36
- @queue = Queue.new
37
- @workers = []
38
- @mutex = Mutex.new
39
- @completion_condition = ConditionVariable.new
40
- @pending_count = 0
41
- @completed_count = 0
42
- @aborted = false
43
- @shutdown = false
44
-
45
- spawn_workers
46
- end
47
-
48
- # Submits a task to the pool
49
- #
50
- # @yield Block to execute in a worker thread
51
- # @return [void]
52
- # @raise [RuntimeError] If pool has been shutdown
53
- def post(&block)
54
- raise "ThreadPool has been shutdown" if @shutdown
55
-
56
- @mutex.synchronize do
57
- @pending_count += 1
58
- end
59
-
60
- @queue.push(block)
61
- end
62
-
63
- # Signals workers to abort remaining tasks
64
- #
65
- # Currently running tasks will complete, but pending tasks will be skipped.
66
- #
67
- # @return [void]
68
- def abort!
69
- @mutex.synchronize do
70
- @aborted = true
71
- end
72
- end
73
-
74
- # Returns whether the pool has been aborted
75
- #
76
- # @return [Boolean] true if abort! was called
77
- def aborted?
78
- @mutex.synchronize { @aborted }
79
- end
80
-
81
- # Waits for all submitted tasks to complete
82
- #
83
- # @param timeout [Integer, nil] Maximum seconds to wait (nil = indefinite)
84
- # @return [Boolean] true if all tasks completed, false if timeout
85
- def wait_for_completion(timeout: nil)
86
- deadline = timeout ? Time.current + timeout : nil
87
-
88
- @mutex.synchronize do
89
- loop do
90
- return true if @pending_count == @completed_count
91
-
92
- if deadline
93
- remaining = deadline - Time.current
94
- return false if remaining <= 0
95
-
96
- @completion_condition.wait(@mutex, remaining)
97
- else
98
- @completion_condition.wait(@mutex)
99
- end
100
- end
101
- end
102
- end
103
-
104
- # Shuts down the pool and waits for workers to terminate
105
- #
106
- # @param timeout [Integer] Maximum seconds to wait for termination
107
- # @return [void]
108
- def shutdown(timeout: 5)
109
- @shutdown = true
110
-
111
- # Send poison pills to stop workers
112
- @size.times { @queue.push(nil) }
113
-
114
- wait_for_termination(timeout: timeout)
115
- end
116
-
117
- # Waits for all worker threads to terminate
118
- #
119
- # @param timeout [Integer] Maximum seconds to wait
120
- # @return [void]
121
- def wait_for_termination(timeout: 5)
122
- deadline = Time.current + timeout
123
-
124
- @workers.each do |worker|
125
- remaining = deadline - Time.current
126
- break if remaining <= 0
127
-
128
- worker.join(remaining)
129
- end
130
- end
131
-
132
- private
133
-
134
- # Spawns the worker threads
135
- #
136
- # @return [void]
137
- def spawn_workers
138
- @size.times do |i|
139
- @workers << Thread.new do
140
- Thread.current.name = "pool-worker-#{i}"
141
- worker_loop
142
- end
143
- end
144
- end
145
-
146
- # Main worker loop - processes tasks from the queue
147
- #
148
- # @return [void]
149
- def worker_loop
150
- loop do
151
- task = @queue.pop
152
-
153
- # nil is the poison pill - time to exit
154
- break if task.nil?
155
-
156
- # Skip if aborted
157
- if aborted?
158
- mark_completed
159
- next
160
- end
161
-
162
- begin
163
- task.call
164
- rescue StandardError
165
- # Errors are handled by the task itself (via rescue in the block)
166
- # We just need to ensure we mark completion
167
- ensure
168
- mark_completed
169
- end
170
- end
171
- end
172
-
173
- # Marks a task as completed and signals waiters
174
- #
175
- # @return [void]
176
- def mark_completed
177
- @mutex.synchronize do
178
- @completed_count += 1
179
- @completion_condition.broadcast
180
- end
181
- end
182
- end
183
- end
184
- end
185
- end
@@ -1,206 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module Agents
5
- class Workflow
6
- # Manages rate limiting and throttling for workflow steps
7
- #
8
- # Provides two modes of rate limiting:
9
- # 1. Throttle: Ensures minimum time between executions of the same step
10
- # 2. Rate limit: Limits the number of calls within a time window (token bucket)
11
- #
12
- # Thread-safe using a Mutex for concurrent access.
13
- #
14
- # @example Using throttle
15
- # manager = ThrottleManager.new
16
- # manager.throttle("step:fetch", 1.0) # Wait at least 1 second between calls
17
- #
18
- # @example Using rate limit
19
- # manager = ThrottleManager.new
20
- # manager.rate_limit("api:external", calls: 10, per: 60) # 10 calls per minute
21
- #
22
- # @api private
23
- class ThrottleManager
24
- def initialize
25
- @last_execution = {}
26
- @rate_limiters = {}
27
- @mutex = Mutex.new
28
- end
29
-
30
- # Throttle execution to ensure minimum time between calls
31
- #
32
- # Blocks the current thread if necessary to maintain the minimum interval.
33
- #
34
- # @param key [String] Unique identifier for the throttle target
35
- # @param duration [Float, Integer] Minimum seconds between executions
36
- # @return [Float] Actual seconds waited (0 if no wait needed)
37
- def throttle(key, duration)
38
- duration_seconds = normalize_duration(duration)
39
-
40
- @mutex.synchronize do
41
- last = @last_execution[key]
42
- waited = 0
43
-
44
- if last
45
- elapsed = Time.now - last
46
- remaining = duration_seconds - elapsed
47
-
48
- if remaining > 0
49
- @mutex.sleep(remaining)
50
- waited = remaining
51
- end
52
- end
53
-
54
- @last_execution[key] = Time.now
55
- waited
56
- end
57
- end
58
-
59
- # Check if a call would be throttled without actually waiting
60
- #
61
- # @param key [String] Unique identifier for the throttle target
62
- # @param duration [Float, Integer] Minimum seconds between executions
63
- # @return [Float] Seconds until next allowed execution (0 if ready)
64
- def throttle_remaining(key, duration)
65
- duration_seconds = normalize_duration(duration)
66
-
67
- @mutex.synchronize do
68
- last = @last_execution[key]
69
- return 0 unless last
70
-
71
- elapsed = Time.now - last
72
- remaining = duration_seconds - elapsed
73
- [remaining, 0].max
74
- end
75
- end
76
-
77
- # Apply rate limiting using a token bucket algorithm
78
- #
79
- # Blocks until a token is available if the rate limit is exceeded.
80
- #
81
- # @param key [String] Unique identifier for the rate limit target
82
- # @param calls [Integer] Number of calls allowed per window
83
- # @param per [Float, Integer] Time window in seconds
84
- # @return [Float] Seconds waited (0 if no wait needed)
85
- def rate_limit(key, calls:, per:)
86
- per_seconds = normalize_duration(per)
87
- bucket = get_or_create_bucket(key, calls, per_seconds)
88
-
89
- @mutex.synchronize do
90
- waited = bucket.acquire
91
- waited
92
- end
93
- end
94
-
95
- # Check if a call would be rate limited without consuming a token
96
- #
97
- # @param key [String] Unique identifier for the rate limit target
98
- # @param calls [Integer] Number of calls allowed per window
99
- # @param per [Float, Integer] Time window in seconds
100
- # @return [Boolean] true if a call would be allowed immediately
101
- def rate_limit_available?(key, calls:, per:)
102
- per_seconds = normalize_duration(per)
103
- bucket = get_or_create_bucket(key, calls, per_seconds)
104
-
105
- @mutex.synchronize do
106
- bucket.available?
107
- end
108
- end
109
-
110
- # Reset throttle state for a specific key
111
- #
112
- # @param key [String] The throttle key to reset
113
- # @return [void]
114
- def reset_throttle(key)
115
- @mutex.synchronize do
116
- @last_execution.delete(key)
117
- end
118
- end
119
-
120
- # Reset rate limiter state for a specific key
121
- #
122
- # @param key [String] The rate limiter key to reset
123
- # @return [void]
124
- def reset_rate_limit(key)
125
- @mutex.synchronize do
126
- @rate_limiters.delete(key)
127
- end
128
- end
129
-
130
- # Reset all throttle and rate limit state
131
- #
132
- # @return [void]
133
- def reset_all!
134
- @mutex.synchronize do
135
- @last_execution.clear
136
- @rate_limiters.clear
137
- end
138
- end
139
-
140
- private
141
-
142
- def normalize_duration(duration)
143
- if duration.respond_to?(:to_f)
144
- duration.to_f
145
- else
146
- duration.to_i.to_f
147
- end
148
- end
149
-
150
- def get_or_create_bucket(key, calls, per)
151
- @rate_limiters[key] ||= TokenBucket.new(calls, per)
152
- end
153
-
154
- # Simple token bucket implementation for rate limiting
155
- #
156
- # @api private
157
- class TokenBucket
158
- def initialize(capacity, refill_time)
159
- @capacity = capacity
160
- @refill_time = refill_time
161
- @tokens = capacity.to_f
162
- @last_refill = Time.now
163
- end
164
-
165
- # Try to acquire a token, waiting if necessary
166
- #
167
- # @return [Float] Seconds waited
168
- def acquire
169
- refill
170
- waited = 0
171
-
172
- if @tokens < 1
173
- # Calculate wait time for next token
174
- tokens_needed = 1 - @tokens
175
- wait_time = tokens_needed * @refill_time / @capacity
176
- sleep(wait_time)
177
- waited = wait_time
178
- refill
179
- end
180
-
181
- @tokens -= 1
182
- waited
183
- end
184
-
185
- # Check if a token is available without consuming it
186
- #
187
- # @return [Boolean]
188
- def available?
189
- refill
190
- @tokens >= 1
191
- end
192
-
193
- private
194
-
195
- def refill
196
- now = Time.now
197
- elapsed = now - @last_refill
198
- refill_amount = elapsed * @capacity / @refill_time
199
- @tokens = [@tokens + refill_amount, @capacity].min
200
- @last_refill = now
201
- end
202
- end
203
- end
204
- end
205
- end
206
- end
@@ -1,213 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module Agents
5
- class Workflow
6
- # Result object for wait step execution
7
- #
8
- # Encapsulates the outcome of a wait operation including success/failure status,
9
- # duration waited, and any metadata like approval details.
10
- #
11
- # @example Success result
12
- # WaitResult.success(:delay, 5.0)
13
- #
14
- # @example Timeout result
15
- # WaitResult.timeout(:until, 60.0, :fail)
16
- #
17
- # @example Approval result
18
- # WaitResult.approved("approval-123", "user@example.com", 3600.0)
19
- #
20
- # @api private
21
- class WaitResult
22
- STATUSES = %i[success timeout approved rejected skipped].freeze
23
-
24
- attr_reader :type, :status, :waited_duration, :metadata
25
-
26
- # @param type [Symbol] Wait type (:delay, :until, :schedule, :approval)
27
- # @param status [Symbol] Result status (:success, :timeout, :approved, :rejected, :skipped)
28
- # @param waited_duration [Float, nil] Duration waited in seconds
29
- # @param metadata [Hash] Additional result metadata
30
- def initialize(type:, status:, waited_duration: nil, metadata: {})
31
- @type = type
32
- @status = status
33
- @waited_duration = waited_duration
34
- @metadata = metadata
35
- end
36
-
37
- # Creates a success result
38
- #
39
- # @param type [Symbol] Wait type
40
- # @param waited_duration [Float] Duration waited
41
- # @param metadata [Hash] Additional metadata
42
- # @return [WaitResult]
43
- def self.success(type, waited_duration, **metadata)
44
- new(
45
- type: type,
46
- status: :success,
47
- waited_duration: waited_duration,
48
- metadata: metadata
49
- )
50
- end
51
-
52
- # Creates a timeout result
53
- #
54
- # @param type [Symbol] Wait type
55
- # @param waited_duration [Float] Duration waited before timeout
56
- # @param action_taken [Symbol] Action taken on timeout (:fail, :continue, :skip_next)
57
- # @param metadata [Hash] Additional metadata
58
- # @return [WaitResult]
59
- def self.timeout(type, waited_duration, action_taken, **metadata)
60
- new(
61
- type: type,
62
- status: :timeout,
63
- waited_duration: waited_duration,
64
- metadata: metadata.merge(action_taken: action_taken)
65
- )
66
- end
67
-
68
- # Creates a skipped result (when condition not met)
69
- #
70
- # @param type [Symbol] Wait type
71
- # @param reason [String, nil] Reason for skipping
72
- # @return [WaitResult]
73
- def self.skipped(type, reason: nil)
74
- new(
75
- type: type,
76
- status: :skipped,
77
- waited_duration: 0,
78
- metadata: { reason: reason }.compact
79
- )
80
- end
81
-
82
- # Creates an approved result for approval waits
83
- #
84
- # @param approval_id [String] Approval identifier
85
- # @param approved_by [String] User who approved
86
- # @param waited_duration [Float] Duration waited for approval
87
- # @param metadata [Hash] Additional metadata
88
- # @return [WaitResult]
89
- def self.approved(approval_id, approved_by, waited_duration, **metadata)
90
- new(
91
- type: :approval,
92
- status: :approved,
93
- waited_duration: waited_duration,
94
- metadata: metadata.merge(
95
- approval_id: approval_id,
96
- approved_by: approved_by
97
- )
98
- )
99
- end
100
-
101
- # Creates a rejected result for approval waits
102
- #
103
- # @param approval_id [String] Approval identifier
104
- # @param rejected_by [String] User who rejected
105
- # @param waited_duration [Float] Duration waited before rejection
106
- # @param reason [String, nil] Rejection reason
107
- # @param metadata [Hash] Additional metadata
108
- # @return [WaitResult]
109
- def self.rejected(approval_id, rejected_by, waited_duration, reason: nil, **metadata)
110
- new(
111
- type: :approval,
112
- status: :rejected,
113
- waited_duration: waited_duration,
114
- metadata: metadata.merge(
115
- approval_id: approval_id,
116
- rejected_by: rejected_by,
117
- reason: reason
118
- ).compact
119
- )
120
- end
121
-
122
- # Returns whether the wait completed successfully
123
- #
124
- # @return [Boolean]
125
- def success?
126
- status == :success || status == :approved
127
- end
128
-
129
- # Returns whether the wait timed out
130
- #
131
- # @return [Boolean]
132
- def timeout?
133
- status == :timeout
134
- end
135
-
136
- # Returns whether the wait was skipped
137
- #
138
- # @return [Boolean]
139
- def skipped?
140
- status == :skipped
141
- end
142
-
143
- # Returns whether an approval was granted
144
- #
145
- # @return [Boolean]
146
- def approved?
147
- status == :approved
148
- end
149
-
150
- # Returns whether an approval was rejected
151
- #
152
- # @return [Boolean]
153
- def rejected?
154
- status == :rejected
155
- end
156
-
157
- # Returns whether the workflow should continue after this wait
158
- #
159
- # @return [Boolean]
160
- def should_continue?
161
- success? || skipped? || (timeout? && metadata[:action_taken] == :continue)
162
- end
163
-
164
- # Returns whether the next step should be skipped
165
- #
166
- # @return [Boolean]
167
- def should_skip_next?
168
- timeout? && metadata[:action_taken] == :skip_next
169
- end
170
-
171
- # Returns the action taken on timeout
172
- #
173
- # @return [Symbol, nil]
174
- def timeout_action
175
- metadata[:action_taken]
176
- end
177
-
178
- # Returns the approval ID for approval waits
179
- #
180
- # @return [String, nil]
181
- def approval_id
182
- metadata[:approval_id]
183
- end
184
-
185
- # Returns who approved/rejected for approval waits
186
- #
187
- # @return [String, nil]
188
- def actor
189
- metadata[:approved_by] || metadata[:rejected_by]
190
- end
191
-
192
- # Returns the rejection reason
193
- #
194
- # @return [String, nil]
195
- def rejection_reason
196
- metadata[:reason]
197
- end
198
-
199
- # Converts to hash for serialization
200
- #
201
- # @return [Hash]
202
- def to_h
203
- {
204
- type: type,
205
- status: status,
206
- waited_duration: waited_duration,
207
- metadata: metadata
208
- }
209
- end
210
- end
211
- end
212
- end
213
- end