agentic 0.1.0 → 0.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.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/.agentic.yml +2 -0
  3. data/.architecture/decisions/ArchitecturalFeatureBuilder.md +136 -0
  4. data/.architecture/decisions/ArchitectureConsiderations.md +200 -0
  5. data/.architecture/decisions/adr_001_observer_pattern_implementation.md +196 -0
  6. data/.architecture/decisions/adr_002_plan_orchestrator.md +320 -0
  7. data/.architecture/decisions/adr_003_plan_orchestrator_interface.md +179 -0
  8. data/.architecture/decisions/adrs/ADR-001-dependency-management.md +147 -0
  9. data/.architecture/decisions/adrs/ADR-002-system-boundaries.md +162 -0
  10. data/.architecture/decisions/adrs/ADR-003-content-safety.md +158 -0
  11. data/.architecture/decisions/adrs/ADR-004-agent-permissions.md +161 -0
  12. data/.architecture/decisions/adrs/ADR-005-adaptation-engine.md +127 -0
  13. data/.architecture/decisions/adrs/ADR-006-extension-system.md +273 -0
  14. data/.architecture/decisions/adrs/ADR-007-learning-system.md +156 -0
  15. data/.architecture/decisions/adrs/ADR-008-prompt-generation.md +325 -0
  16. data/.architecture/decisions/adrs/ADR-009-task-failure-handling.md +353 -0
  17. data/.architecture/decisions/adrs/ADR-010-task-input-handling.md +251 -0
  18. data/.architecture/decisions/adrs/ADR-011-task-observable-pattern.md +391 -0
  19. data/.architecture/decisions/adrs/ADR-012-task-output-handling.md +205 -0
  20. data/.architecture/decisions/adrs/ADR-013-architecture-alignment.md +211 -0
  21. data/.architecture/decisions/adrs/ADR-014-agent-capability-registry.md +80 -0
  22. data/.architecture/decisions/adrs/ADR-015-persistent-agent-store.md +100 -0
  23. data/.architecture/decisions/adrs/ADR-016-agent-assembly-engine.md +117 -0
  24. data/.architecture/decisions/adrs/ADR-017-streaming-observability.md +171 -0
  25. data/.architecture/decisions/capability_tools_distinction.md +150 -0
  26. data/.architecture/decisions/cli_command_structure.md +61 -0
  27. data/.architecture/implementation/agent_self_assembly_implementation.md +267 -0
  28. data/.architecture/implementation/agent_self_assembly_summary.md +138 -0
  29. data/.architecture/members.yml +187 -0
  30. data/.architecture/planning/self_implementation_exercise.md +295 -0
  31. data/.architecture/planning/session_compaction_rule.md +43 -0
  32. data/.architecture/planning/streaming_observability_feature.md +223 -0
  33. data/.architecture/principles.md +151 -0
  34. data/.architecture/recalibration/0-2-0.md +92 -0
  35. data/.architecture/recalibration/agent_self_assembly.md +238 -0
  36. data/.architecture/recalibration/cli_command_structure.md +91 -0
  37. data/.architecture/recalibration/implementation_roadmap_0-2-0.md +301 -0
  38. data/.architecture/recalibration/progress_tracking_0-2-0.md +114 -0
  39. data/.architecture/recalibration_process.md +127 -0
  40. data/.architecture/reviews/0-2-0.md +181 -0
  41. data/.architecture/reviews/cli_command_duplication.md +98 -0
  42. data/.architecture/templates/adr.md +105 -0
  43. data/.architecture/templates/implementation_roadmap.md +125 -0
  44. data/.architecture/templates/progress_tracking.md +89 -0
  45. data/.architecture/templates/recalibration_plan.md +70 -0
  46. data/.architecture/templates/version_comparison.md +124 -0
  47. data/.claude/settings.local.json +13 -0
  48. data/.claude-sessions/001-task-class-architecture-implementation.md +129 -0
  49. data/.claude-sessions/002-plan-orchestrator-interface-review.md +105 -0
  50. data/.claude-sessions/architecture-governance-implementation.md +37 -0
  51. data/.claude-sessions/architecture-review-session.md +27 -0
  52. data/ArchitecturalFeatureBuilder.md +136 -0
  53. data/ArchitectureConsiderations.md +229 -0
  54. data/CHANGELOG.md +57 -2
  55. data/CLAUDE.md +111 -0
  56. data/CONTRIBUTING.md +286 -0
  57. data/MAINTAINING.md +301 -0
  58. data/README.md +582 -28
  59. data/docs/agent_capabilities_api.md +259 -0
  60. data/docs/artifact_extension_points.md +757 -0
  61. data/docs/artifact_generation_architecture.md +323 -0
  62. data/docs/artifact_implementation_plan.md +596 -0
  63. data/docs/artifact_integration_points.md +345 -0
  64. data/docs/artifact_verification_strategies.md +581 -0
  65. data/docs/streaming_observability_architecture.md +510 -0
  66. data/exe/agentic +6 -1
  67. data/lefthook.yml +5 -0
  68. data/lib/agentic/adaptation_engine.rb +124 -0
  69. data/lib/agentic/agent.rb +181 -4
  70. data/lib/agentic/agent_assembly_engine.rb +442 -0
  71. data/lib/agentic/agent_capability_registry.rb +260 -0
  72. data/lib/agentic/agent_config.rb +63 -0
  73. data/lib/agentic/agent_specification.rb +46 -0
  74. data/lib/agentic/capabilities/examples.rb +530 -0
  75. data/lib/agentic/capabilities.rb +14 -0
  76. data/lib/agentic/capability_provider.rb +146 -0
  77. data/lib/agentic/capability_specification.rb +118 -0
  78. data/lib/agentic/cli/agent.rb +31 -0
  79. data/lib/agentic/cli/capabilities.rb +191 -0
  80. data/lib/agentic/cli/config.rb +134 -0
  81. data/lib/agentic/cli/execution_observer.rb +796 -0
  82. data/lib/agentic/cli.rb +1068 -0
  83. data/lib/agentic/default_agent_provider.rb +35 -0
  84. data/lib/agentic/errors/llm_error.rb +184 -0
  85. data/lib/agentic/execution_plan.rb +53 -0
  86. data/lib/agentic/execution_result.rb +91 -0
  87. data/lib/agentic/expected_answer_format.rb +46 -0
  88. data/lib/agentic/extension/domain_adapter.rb +109 -0
  89. data/lib/agentic/extension/plugin_manager.rb +163 -0
  90. data/lib/agentic/extension/protocol_handler.rb +116 -0
  91. data/lib/agentic/extension.rb +45 -0
  92. data/lib/agentic/factory_methods.rb +9 -1
  93. data/lib/agentic/generation_stats.rb +61 -0
  94. data/lib/agentic/learning/README.md +84 -0
  95. data/lib/agentic/learning/capability_optimizer.rb +613 -0
  96. data/lib/agentic/learning/execution_history_store.rb +251 -0
  97. data/lib/agentic/learning/pattern_recognizer.rb +500 -0
  98. data/lib/agentic/learning/strategy_optimizer.rb +706 -0
  99. data/lib/agentic/learning.rb +131 -0
  100. data/lib/agentic/llm_assisted_composition_strategy.rb +188 -0
  101. data/lib/agentic/llm_client.rb +215 -15
  102. data/lib/agentic/llm_config.rb +65 -1
  103. data/lib/agentic/llm_response.rb +163 -0
  104. data/lib/agentic/logger.rb +1 -1
  105. data/lib/agentic/observable.rb +51 -0
  106. data/lib/agentic/persistent_agent_store.rb +385 -0
  107. data/lib/agentic/plan_execution_result.rb +129 -0
  108. data/lib/agentic/plan_orchestrator.rb +464 -0
  109. data/lib/agentic/plan_orchestrator_config.rb +57 -0
  110. data/lib/agentic/retry_config.rb +63 -0
  111. data/lib/agentic/retry_handler.rb +125 -0
  112. data/lib/agentic/structured_outputs.rb +1 -1
  113. data/lib/agentic/task.rb +193 -0
  114. data/lib/agentic/task_definition.rb +39 -0
  115. data/lib/agentic/task_execution_result.rb +92 -0
  116. data/lib/agentic/task_failure.rb +66 -0
  117. data/lib/agentic/task_output_schemas.rb +112 -0
  118. data/lib/agentic/task_planner.rb +54 -19
  119. data/lib/agentic/task_result.rb +48 -0
  120. data/lib/agentic/ui.rb +244 -0
  121. data/lib/agentic/verification/critic_framework.rb +116 -0
  122. data/lib/agentic/verification/llm_verification_strategy.rb +60 -0
  123. data/lib/agentic/verification/schema_verification_strategy.rb +47 -0
  124. data/lib/agentic/verification/verification_hub.rb +62 -0
  125. data/lib/agentic/verification/verification_result.rb +50 -0
  126. data/lib/agentic/verification/verification_strategy.rb +26 -0
  127. data/lib/agentic/version.rb +1 -1
  128. data/lib/agentic.rb +74 -2
  129. data/plugins/README.md +41 -0
  130. metadata +245 -6
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "task_execution_result"
4
+
5
+ module Agentic
6
+ # Value object representing the execution result of a plan
7
+ class PlanExecutionResult
8
+ # @return [String] The unique identifier for the plan
9
+ attr_reader :plan_id
10
+
11
+ # @return [Symbol] The overall status of the plan (:completed, :in_progress, :partial_failure)
12
+ attr_reader :status
13
+
14
+ # @return [Float] The execution time in seconds
15
+ attr_reader :execution_time
16
+
17
+ # @return [Hash] Map of task ids to serialized Task objects
18
+ attr_reader :tasks
19
+
20
+ # @return [Hash<String, TaskExecutionResult>] The execution results for each task
21
+ attr_reader :results
22
+
23
+ # Initializes a new plan execution result
24
+ # @param plan_id [String] The unique identifier for the plan
25
+ # @param status [Symbol] The overall status of the plan
26
+ # @param execution_time [Float] The execution time in seconds
27
+ # @param tasks [Hash] Map of task ids to serialized Task objects
28
+ # @param results [Hash] Map of task ids to raw execution results
29
+ def initialize(plan_id:, status:, execution_time:, tasks:, results:)
30
+ @plan_id = plan_id
31
+ @status = status
32
+ @execution_time = execution_time
33
+ @tasks = tasks
34
+ @results = convert_raw_results(results)
35
+ end
36
+
37
+ # Creates a plan execution result from a hash
38
+ # @param hash [Hash] The hash representation of a plan execution result
39
+ # @return [PlanExecutionResult] A plan execution result
40
+ def self.from_hash(hash)
41
+ new(
42
+ plan_id: hash[:plan_id],
43
+ status: hash[:status],
44
+ execution_time: hash[:execution_time],
45
+ tasks: hash[:tasks],
46
+ results: hash[:results]
47
+ )
48
+ end
49
+
50
+ # Checks if the plan execution was successful
51
+ # @return [Boolean] True if successful, false otherwise
52
+ def successful?
53
+ @status == :completed
54
+ end
55
+
56
+ # Checks if the plan execution failed partially
57
+ # @return [Boolean] True if partially failed, false otherwise
58
+ def partial_failure?
59
+ @status == :partial_failure
60
+ end
61
+
62
+ # Checks if the plan execution is still in progress
63
+ # @return [Boolean] True if in progress, false otherwise
64
+ def in_progress?
65
+ @status == :in_progress
66
+ end
67
+
68
+ # Gets the result for a specific task
69
+ # @param task_id [String] The ID of the task
70
+ # @return [TaskExecutionResult, nil] The execution result for the task, or nil if not found
71
+ def task_result(task_id)
72
+ @results[task_id]
73
+ end
74
+
75
+ # Gets the serialized task data for a specific task
76
+ # @param task_id [String] The ID of the task
77
+ # @return [Hash, nil] The serialized task data, or nil if not found
78
+ def task_data(task_id)
79
+ @tasks[task_id]
80
+ end
81
+
82
+ # Gets the number of completed tasks
83
+ # @return [Integer] The number of completed tasks
84
+ def completed_tasks_count
85
+ @results.count { |_, result| result.successful? }
86
+ end
87
+
88
+ # Gets the number of failed tasks
89
+ # @return [Integer] The number of failed tasks
90
+ def failed_tasks_count
91
+ @results.count { |_, result| result.failed? }
92
+ end
93
+
94
+ # Gets the successful task results
95
+ # @return [Hash<String, TaskExecutionResult>] The successful task results
96
+ def successful_task_results
97
+ @results.select { |_, result| result.successful? }
98
+ end
99
+
100
+ # Gets the failed task results
101
+ # @return [Hash<String, TaskExecutionResult>] The failed task results
102
+ def failed_task_results
103
+ @results.select { |_, result| result.failed? }
104
+ end
105
+
106
+ # Returns a hash representation of the plan execution result
107
+ # @return [Hash] The plan execution result as a hash
108
+ def to_h
109
+ {
110
+ plan_id: @plan_id,
111
+ status: @status,
112
+ execution_time: @execution_time,
113
+ tasks: @tasks,
114
+ results: @results.transform_values(&:to_h)
115
+ }
116
+ end
117
+
118
+ private
119
+
120
+ # Converts raw results to TaskExecutionResult objects
121
+ # @param raw_results [Hash] Map of task ids to raw execution results
122
+ # @return [Hash<String, TaskExecutionResult>] The converted results
123
+ def convert_raw_results(raw_results)
124
+ raw_results.transform_values do |result|
125
+ result.is_a?(TaskExecutionResult) ? result : TaskExecutionResult.from_hash(result)
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,464 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "set"
5
+ require "async"
6
+ require "async/barrier"
7
+ require "async/semaphore"
8
+ require_relative "task_failure"
9
+ require_relative "task_execution_result"
10
+ require_relative "plan_execution_result"
11
+
12
+ module Agentic
13
+ # Orchestrates the execution of tasks in a plan, handling dependencies and concurrency
14
+ # @attr_reader [String] plan_id Unique identifier for the plan
15
+ # @attr_reader [Hash] tasks Map of task ids to Task objects
16
+ # @attr_reader [Hash] execution_state Current state of all tasks in the plan
17
+ # @attr_reader [Hash] results Results of task execution
18
+ class PlanOrchestrator
19
+ attr_reader :plan_id, :tasks, :execution_state, :results, :retry_policy, :lifecycle_hooks
20
+
21
+ # Initializes a new plan orchestrator
22
+ # @param plan_id [String] Optional plan id, will be generated if not provided
23
+ # @param concurrency_limit [Integer] Maximum number of tasks to execute concurrently
24
+ # @param retry_policy [Hash] Configuration for retry behavior
25
+ # @param lifecycle_hooks [Hash] Configuration for execution lifecycle hooks
26
+ # @return [PlanOrchestrator] A new plan orchestrator instance
27
+ def initialize(plan_id: SecureRandom.uuid, concurrency_limit: 10, retry_policy: {}, lifecycle_hooks: {})
28
+ @plan_id = plan_id
29
+ @tasks = {}
30
+ @dependencies = {}
31
+ @results = {}
32
+ @execution_state = {
33
+ pending: Set.new,
34
+ in_progress: Set.new,
35
+ completed: Set.new,
36
+ failed: Set.new,
37
+ canceled: Set.new
38
+ }
39
+ @concurrency_limit = concurrency_limit
40
+ @async_tasks = {}
41
+
42
+ # Configure retry policy with defaults
43
+ @retry_policy = {
44
+ max_retries: 3,
45
+ retryable_errors: ["TimeoutError"],
46
+ backoff_strategy: :constant
47
+ }.merge(retry_policy)
48
+
49
+ # Configure lifecycle hooks with callable defaults (no-ops)
50
+ @lifecycle_hooks = {
51
+ before_agent_build: ->(task_id:, task:) {}, # Called before an agent is built
52
+ after_agent_build: ->(task_id:, task:, agent:, build_duration:) {}, # Called after an agent is built
53
+ before_task_execution: ->(task_id:, task:) {}, # Called before a task is executed
54
+ after_task_success: ->(task_id:, task:, result:, duration:) {}, # Called after a task succeeds
55
+ after_task_failure: ->(task_id:, task:, failure:, duration:) {}, # Called after a task fails
56
+ plan_completed: ->(plan_id:, status:, execution_time:, tasks:, results:) {} # Called when plan completes
57
+ }.merge(lifecycle_hooks)
58
+ end
59
+
60
+ # Adds a task to the plan with optional dependencies
61
+ # @param task [Task] The task to add
62
+ # @param dependencies [Array<String>] Array of task ids that this task depends on
63
+ # @return [void]
64
+ def add_task(task, dependencies = [])
65
+ task_id = task.id
66
+ @tasks[task_id] = task
67
+ @dependencies[task_id] = Array(dependencies)
68
+ @execution_state[:pending].add(task_id)
69
+ end
70
+
71
+ # Executes the plan, respecting task dependencies and concurrency limits
72
+ # @param agent_provider [Object] An object that provides agents for task execution
73
+ # @return [PlanExecutionResult] The structured execution results
74
+ def execute_plan(agent_provider)
75
+ @reactor = Async do |reactor|
76
+ @barrier = Async::Barrier.new
77
+ @semaphore = Async::Semaphore.new(@concurrency_limit, parent: @barrier)
78
+
79
+ # Track execution start time
80
+ @execution_start_time = Time.now
81
+
82
+ # Start with tasks that have no dependencies
83
+ eligible_tasks = find_eligible_tasks
84
+
85
+ # Initial execution of eligible tasks
86
+ eligible_tasks.each do |task_id|
87
+ schedule_task(task_id, agent_provider, @semaphore, @barrier)
88
+ end
89
+
90
+ # Wait for all tasks to complete
91
+ @barrier.wait
92
+
93
+ # Track execution completion time
94
+ @execution_end_time = Time.now
95
+
96
+ # Call plan completion hook
97
+ @lifecycle_hooks[:plan_completed].call(
98
+ plan_id: @plan_id,
99
+ status: overall_status,
100
+ execution_time: @execution_end_time - @execution_start_time,
101
+ tasks: @tasks.transform_values(&:to_h),
102
+ results: @results
103
+ )
104
+ ensure
105
+ @barrier&.stop
106
+ # Ensure execution_end_time is set even if an exception occurred
107
+ @execution_end_time ||= Time.now
108
+ end
109
+
110
+ # Create and return a PlanExecutionResult
111
+ PlanExecutionResult.new(
112
+ plan_id: @plan_id,
113
+ status: overall_status,
114
+ execution_time: @execution_end_time - @execution_start_time,
115
+ tasks: @tasks.transform_values(&:to_h),
116
+ results: @results
117
+ )
118
+ end
119
+
120
+ # Cancels execution of a specific task
121
+ # @param task_id [String] ID of the task to cancel
122
+ # @return [Boolean] True if the task was canceled, false otherwise
123
+ def cancel_task(task_id)
124
+ # Can only cancel pending or in_progress tasks
125
+ return false unless @execution_state[:pending].include?(task_id) ||
126
+ @execution_state[:in_progress].include?(task_id)
127
+
128
+ # If the task is pending, simply move it to canceled state
129
+ if @execution_state[:pending].include?(task_id)
130
+ transition_task_state(task_id, from: :pending, to: :canceled)
131
+ return true
132
+ end
133
+
134
+ # If the task is in progress, cancel its Async task
135
+ if @execution_state[:in_progress].include?(task_id) && @async_tasks[task_id]
136
+ @async_tasks[task_id].stop
137
+ transition_task_state(task_id, from: :in_progress, to: :canceled)
138
+ return true
139
+ end
140
+
141
+ false
142
+ end
143
+
144
+ # Cancels execution of the entire plan
145
+ # @return [void]
146
+ def cancel_plan
147
+ # Stop the reactor to cancel all async tasks
148
+ @reactor&.stop
149
+
150
+ # Move all pending and in_progress tasks to canceled state
151
+ @execution_state[:pending].each do |task_id|
152
+ transition_task_state(task_id, from: :pending, to: :canceled)
153
+ end
154
+
155
+ @execution_state[:in_progress].each do |task_id|
156
+ transition_task_state(task_id, from: :in_progress, to: :canceled)
157
+ end
158
+ end
159
+
160
+ # Determines if a task failure is retryable based on retry policy
161
+ # @param task [Task] The failed task
162
+ # @param failure [TaskFailure] The failure details
163
+ # @return [Boolean] True if the task failure is retryable
164
+ def retry?(task:, failure:)
165
+ # Check if we've reached max retries
166
+ task.retry_count ||= 0
167
+ return false if task.retry_count >= @retry_policy[:max_retries]
168
+
169
+ # Check if error type is in retryable_errors list
170
+ @retry_policy[:retryable_errors].include?(failure.type)
171
+ end
172
+
173
+ # Determines if a failure requires human intervention
174
+ # @param failure [TaskFailure] The failure details
175
+ # @return [Boolean] True if human intervention is required
176
+ def requires_intervention?(failure:)
177
+ # For now, we only identify a few error types that need human help
178
+ %w[AuthenticationError PermissionDeniedError ConfigurationError].include?(failure.type)
179
+ end
180
+
181
+ # Applies a delay based on the backoff strategy before retrying
182
+ # @param task [Task] The task being retried
183
+ # @return [void]
184
+ def apply_retry_backoff(task:)
185
+ return if @retry_policy[:backoff_strategy] == :none
186
+
187
+ delay = case @retry_policy[:backoff_strategy]
188
+ when :constant
189
+ # Constant delay (default 1 second)
190
+ @retry_policy[:backoff_constant] || 1
191
+ when :linear
192
+ # Linear backoff (retry_count * base_delay)
193
+ base_delay = @retry_policy[:backoff_base] || 1
194
+ task.retry_count * base_delay
195
+ when :exponential
196
+ # Exponential backoff (base_delay * 2^retry_count)
197
+ base_delay = @retry_policy[:backoff_base] || 1
198
+ base_delay * (2**(task.retry_count - 1))
199
+ else
200
+ 0
201
+ end
202
+
203
+ # Apply jitter if configured
204
+ if @retry_policy[:backoff_jitter]
205
+ jitter_factor = 0.25 # Default 25% jitter
206
+ jitter = rand(-delay * jitter_factor..delay * jitter_factor)
207
+ delay += jitter
208
+ end
209
+
210
+ # Sleep if there's a delay to apply
211
+ if delay > 0
212
+ Async do
213
+ Async::Task.current.sleep(delay) if delay > 0
214
+ end
215
+ end
216
+ end
217
+
218
+ # Checks if all dependencies for a task are met
219
+ # @param task_id [String] ID of the task to check
220
+ # @return [Boolean] True if all dependencies are met, false otherwise
221
+ def all_dependencies_met?(task_id)
222
+ deps = @dependencies[task_id] || []
223
+ deps.all? do |dep_id|
224
+ @execution_state[:completed].include?(dep_id)
225
+ end
226
+ end
227
+
228
+ # Finds tasks that are eligible for execution (have no dependencies)
229
+ # @return [Array<String>] IDs of eligible tasks
230
+ def find_eligible_tasks
231
+ @dependencies.select do |task_id, deps|
232
+ deps.empty? && @execution_state[:pending].include?(task_id)
233
+ end.keys
234
+ end
235
+
236
+ # Determines the overall status of the plan
237
+ # @return [Symbol] The overall status (:completed, :in_progress, or :partial_failure)
238
+ def overall_status
239
+ if @execution_state[:failed].any?
240
+ :partial_failure
241
+ elsif @execution_state[:pending].empty? && @execution_state[:in_progress].empty?
242
+ :completed
243
+ else
244
+ :in_progress
245
+ end
246
+ end
247
+
248
+ private
249
+
250
+ # Schedules a task for execution using the semaphore to limit concurrency
251
+ # @param task_id [String] ID of the task to schedule
252
+ # @param agent_provider [Object] Provides agents for task execution
253
+ # @param semaphore [Async::Semaphore] Controls concurrency
254
+ # @param barrier [Async::Barrier] Tracks task completion
255
+ # @return [void]
256
+ def schedule_task(task_id, agent_provider, semaphore, barrier)
257
+ return unless @execution_state[:pending].include?(task_id)
258
+
259
+ # Move to in_progress state
260
+ task = @tasks[task_id]
261
+ transition_task_state(task_id, from: :pending, to: :in_progress)
262
+
263
+ # Call before_task_execution hook
264
+ @lifecycle_hooks[:before_task_execution].call(
265
+ task_id: task_id,
266
+ task: task
267
+ )
268
+
269
+ # Schedule task execution with the semaphore
270
+ async_task = semaphore.async do
271
+ task_start_time = Time.now
272
+ begin
273
+ # Call before_agent_build hook
274
+ @lifecycle_hooks[:before_agent_build].call(
275
+ task_id: task_id,
276
+ task: task
277
+ )
278
+
279
+ agent_build_start = Time.now
280
+ agent = agent_provider.get_agent_for_task(task)
281
+ agent_build_duration = Time.now - agent_build_start
282
+
283
+ # Call after_agent_build hook
284
+ @lifecycle_hooks[:after_agent_build].call(
285
+ task_id: task_id,
286
+ task: task,
287
+ agent: agent,
288
+ build_duration: agent_build_duration
289
+ )
290
+
291
+ result = task.perform(agent)
292
+ task_duration = Time.now - task_start_time
293
+
294
+ # Record result and update state
295
+ if result.successful?
296
+ record_task_success(task_id, result.output)
297
+
298
+ # Call after_task_success hook
299
+ @lifecycle_hooks[:after_task_success].call(
300
+ task_id: task_id,
301
+ task: task,
302
+ result: result,
303
+ duration: task_duration
304
+ )
305
+
306
+ # Find and schedule dependent tasks
307
+ schedule_dependent_tasks(task_id, agent_provider, semaphore, barrier)
308
+ else
309
+ record_task_failure(task_id, result.failure)
310
+
311
+ # Call after_task_failure hook
312
+ @lifecycle_hooks[:after_task_failure].call(
313
+ task_id: task_id,
314
+ task: task,
315
+ failure: result.failure,
316
+ duration: task_duration
317
+ )
318
+
319
+ # Handle failure based on policy
320
+ handle_task_failure(task, result.failure, agent_provider, semaphore, barrier)
321
+ end
322
+ rescue => e
323
+ # Handle unexpected errors
324
+ failure = TaskFailure.from_exception(e, {
325
+ task_id: task_id,
326
+ context_type: "unexpected_error"
327
+ })
328
+
329
+ record_task_failure(task_id, failure)
330
+
331
+ # Call after_task_failure hook for unexpected errors
332
+ @lifecycle_hooks[:after_task_failure].call(
333
+ task_id: task_id,
334
+ task: task,
335
+ failure: failure,
336
+ duration: Time.now - task_start_time
337
+ )
338
+
339
+ Agentic.logger.error("Unexpected error in task #{task_id}: #{e.message}")
340
+ end
341
+ end
342
+
343
+ # Store the async task for potential cancellation
344
+ @async_tasks[task_id] = async_task
345
+ end
346
+
347
+ # Schedules tasks that depend on a completed task
348
+ # @param completed_task_id [String] ID of the completed task
349
+ # @param agent_provider [Object] Provides agents for task execution
350
+ # @param semaphore [Async::Semaphore] Controls concurrency
351
+ # @param barrier [Async::Barrier] Tracks task completion
352
+ # @return [void]
353
+ def schedule_dependent_tasks(completed_task_id, agent_provider, semaphore, barrier)
354
+ # Find tasks that depend on the completed task
355
+ dependent_tasks = @dependencies.select do |task_id, deps|
356
+ deps.include?(completed_task_id) && @execution_state[:pending].include?(task_id)
357
+ end.keys
358
+
359
+ # For each dependent task, check if all dependencies are satisfied
360
+ dependent_tasks.each do |task_id|
361
+ @dependencies[task_id]
362
+ all_deps_satisfied = all_dependencies_met?(task_id)
363
+
364
+ if all_deps_satisfied
365
+ schedule_task(task_id, agent_provider, semaphore, barrier)
366
+ end
367
+ end
368
+ end
369
+
370
+ # Handles a task failure according to policy
371
+ # @param task [Task] The failed task
372
+ # @param failure [TaskFailure] The failure details
373
+ # @param agent_provider [Object] Provides agents for task execution
374
+ # @param semaphore [Async::Semaphore] Controls concurrency
375
+ # @param barrier [Async::Barrier] Tracks task completion
376
+ # @return [void]
377
+ def handle_task_failure(task, failure, agent_provider, semaphore, barrier)
378
+ # Check if this error type is retryable according to policy
379
+ if retry?(task: task, failure: failure)
380
+ Agentic.logger.info("Task #{task.id} failed with #{failure.type}, retrying...")
381
+ retry_task(task, agent_provider, semaphore, barrier)
382
+ elsif requires_intervention?(failure: failure)
383
+ Agentic.logger.warn("Task #{task.id} failed with #{failure.type}, intervention required")
384
+ request_human_intervention(task, failure)
385
+ else
386
+ # Apply general failure policy
387
+ Agentic.logger.error("Task #{task.id} failed: #{failure.message}")
388
+ end
389
+ end
390
+
391
+ # Retries a failed task
392
+ # @param task [Task] The failed task
393
+ # @param agent_provider [Object] Provides agents for task execution
394
+ # @param semaphore [Async::Semaphore] Controls concurrency
395
+ # @param barrier [Async::Barrier] Tracks task completion
396
+ # @return [void]
397
+ def retry_task(task, agent_provider, semaphore, barrier)
398
+ # Check if the task can be retried
399
+ return unless task.status == :failed
400
+
401
+ # Initialize retry count if not already set
402
+ task.retry_count ||= 0
403
+
404
+ # Check if max retries reached
405
+ max_retries = @retry_policy[:max_retries]
406
+ if task.retry_count >= max_retries
407
+ Agentic.logger.warn("Max retries reached for task #{task.id}")
408
+ return
409
+ end
410
+
411
+ # Increment retry count
412
+ task.retry_count += 1
413
+ Agentic.logger.info("Retrying task #{task.id} (attempt #{task.retry_count} of #{max_retries})")
414
+
415
+ # Apply backoff delay if specified
416
+ apply_retry_backoff(task: task)
417
+
418
+ # Reset task state for retry
419
+ transition_task_state(task.id, from: :failed, to: :pending)
420
+
421
+ # Schedule retrying the task
422
+ schedule_task(task.id, agent_provider, semaphore, barrier)
423
+ end
424
+
425
+ # Requests human intervention for a failed task
426
+ # @param task [Task] The failed task
427
+ # @param failure [TaskFailure] The failure details
428
+ # @return [void]
429
+ def request_human_intervention(task, failure)
430
+ # This would integrate with the yet-to-be-implemented human intervention system
431
+ Agentic.logger.warn("Human intervention requested for task #{task.id}: #{failure.message}")
432
+ end
433
+
434
+ # Records a successful task completion with proper state transition and result storage
435
+ # @param task_id [String] ID of the completed task
436
+ # @param output [Hash] The task output
437
+ # @return [void]
438
+ def record_task_success(task_id, output)
439
+ transition_task_state(task_id, from: :in_progress, to: :completed)
440
+ @results[task_id] = TaskExecutionResult.success(output)
441
+ end
442
+
443
+ # Records a task failure with proper state transition and result storage
444
+ # @param task_id [String] ID of the failed task
445
+ # @param failure [TaskFailure] The failure details
446
+ # @return [void]
447
+ def record_task_failure(task_id, failure)
448
+ transition_task_state(task_id, from: :in_progress, to: :failed)
449
+ @results[task_id] = TaskExecutionResult.failure(failure)
450
+ end
451
+
452
+ # Transitions a task from one state to another
453
+ # @param task_id [String] ID of the task to transition
454
+ # @param from: [Symbol] Current state of the task
455
+ # @param to: [Symbol] Target state for the task
456
+ # @return [void]
457
+ def transition_task_state(task_id, from:, to:)
458
+ return unless @execution_state[from].include?(task_id)
459
+
460
+ @execution_state[from].delete(task_id)
461
+ @execution_state[to].add(task_id)
462
+ end
463
+ end
464
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agentic
4
+ # Configuration object for the PlanOrchestrator
5
+ class PlanOrchestratorConfig
6
+ # @return [Integer] Maximum number of concurrent tasks
7
+ attr_accessor :concurrency_limit
8
+
9
+ # @return [Hash] Lifecycle hooks for the orchestrator
10
+ attr_accessor :lifecycle_hooks
11
+
12
+ # @return [Boolean] Whether to continue execution after a task failure
13
+ attr_accessor :continue_on_failure
14
+
15
+ # @return [RetryConfig] Retry configuration for tasks
16
+ attr_accessor :retry_config
17
+
18
+ # @return [Boolean] Whether to execute tasks asynchronously
19
+ attr_accessor :async
20
+
21
+ # Initializes a new plan orchestrator configuration
22
+ # @param concurrency_limit [Integer] Maximum number of concurrent tasks
23
+ # @param lifecycle_hooks [Hash] Lifecycle hooks for the orchestrator
24
+ # @param continue_on_failure [Boolean] Whether to continue execution after a task failure
25
+ # @param retry_config [RetryConfig, nil] Retry configuration for tasks
26
+ # @param async [Boolean] Whether to execute tasks asynchronously
27
+ def initialize(
28
+ concurrency_limit: 10,
29
+ lifecycle_hooks: {},
30
+ continue_on_failure: true,
31
+ retry_config: nil,
32
+ async: true
33
+ )
34
+ @concurrency_limit = concurrency_limit
35
+ @lifecycle_hooks = lifecycle_hooks
36
+ @continue_on_failure = continue_on_failure
37
+ @retry_config = retry_config || RetryConfig.new
38
+ @async = async
39
+ end
40
+
41
+ # Returns a hash of configuration options
42
+ # @return [Hash] The configuration options
43
+ def to_h
44
+ {
45
+ concurrency_limit: @concurrency_limit,
46
+ lifecycle_hooks: @lifecycle_hooks,
47
+ continue_on_failure: @continue_on_failure,
48
+ retry_config: {
49
+ max_retries: @retry_config.max_retries,
50
+ backoff_strategy: @retry_config.backoff_strategy,
51
+ backoff_options: @retry_config.backoff_options
52
+ },
53
+ async: @async
54
+ }
55
+ end
56
+ end
57
+ end