aidp 0.34.1 → 0.39.3
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/README.md +14 -14
- data/lib/aidp/agent_harness_adapter.rb +309 -0
- data/lib/aidp/cli/issue_importer.rb +20 -6
- data/lib/aidp/cli/jobs_command.rb +1 -1
- data/lib/aidp/cli/models_command.rb +9 -16
- data/lib/aidp/cli/prompts_command.rb +253 -0
- data/lib/aidp/cli/security_command.rb +4 -10
- data/lib/aidp/cli/storage_command.rb +277 -0
- data/lib/aidp/cli/temporal_command.rb +460 -0
- data/lib/aidp/cli.rb +24 -2
- data/lib/aidp/config.rb +15 -0
- data/lib/aidp/config_paths.rb +12 -2
- data/lib/aidp/database/migrations.rb +114 -0
- data/lib/aidp/database/repositories/checkpoint_repository.rb +174 -0
- data/lib/aidp/database/repositories/deprecated_models_repository.rb +206 -0
- data/lib/aidp/database/repositories/evaluation_repository.rb +208 -0
- data/lib/aidp/database/repositories/harness_state_repository.rb +140 -0
- data/lib/aidp/database/repositories/job_repository.rb +278 -0
- data/lib/aidp/database/repositories/model_cache_repository.rb +194 -0
- data/lib/aidp/database/repositories/progress_repository.rb +210 -0
- data/lib/aidp/database/repositories/prompt_archive_repository.rb +195 -0
- data/lib/aidp/database/repositories/prompt_feedback_repository.rb +266 -0
- data/lib/aidp/database/repositories/provider_info_cache_repository.rb +208 -0
- data/lib/aidp/database/repositories/provider_metrics_repository.rb +245 -0
- data/lib/aidp/database/repositories/secrets_repository.rb +187 -0
- data/lib/aidp/database/repositories/task_repository.rb +231 -0
- data/lib/aidp/database/repositories/template_version_repository.rb +383 -0
- data/lib/aidp/database/repositories/watch_state_repository.rb +472 -0
- data/lib/aidp/database/repositories/workstream_repository.rb +310 -0
- data/lib/aidp/database/repositories/worktree_repository.rb +240 -0
- data/lib/aidp/database/repository.rb +166 -0
- data/lib/aidp/database/schema.rb +347 -0
- data/lib/aidp/database/storage_migrator.rb +693 -0
- data/lib/aidp/database.rb +139 -0
- data/lib/aidp/debug_mixin.rb +2 -2
- data/lib/aidp/execute/checkpoint.rb +26 -0
- data/lib/aidp/execute/checkpoint_display.rb +16 -0
- data/lib/aidp/execute/prompt_evaluator.rb +30 -57
- data/lib/aidp/execute/work_loop_runner.rb +294 -46
- data/lib/aidp/firewall/provider_requirements_collector.rb +7 -23
- data/lib/aidp/harness/agent_harness_provider_manager.rb +275 -0
- data/lib/aidp/harness/ai_decision_engine.rb +78 -296
- data/lib/aidp/harness/config_schema.rb +59 -0
- data/lib/aidp/harness/configuration.rb +229 -10
- data/lib/aidp/harness/deprecation_cache.rb +53 -11
- data/lib/aidp/harness/enhanced_runner.rb +1 -1
- data/lib/aidp/harness/error_handler.rb +11 -5
- data/lib/aidp/harness/provider_config.rb +36 -0
- data/lib/aidp/harness/provider_factory.rb +26 -24
- data/lib/aidp/harness/provider_info.rb +9 -5
- data/lib/aidp/harness/provider_manager.rb +2 -2
- data/lib/aidp/harness/provider_metrics.rb +103 -1
- data/lib/aidp/harness/rspec_command_optimizer.rb +166 -0
- data/lib/aidp/harness/ruby_llm_registry.rb +97 -8
- data/lib/aidp/harness/runner.rb +7 -2
- data/lib/aidp/harness/state_manager.rb +1 -1
- data/lib/aidp/harness/test_runner.rb +167 -11
- data/lib/aidp/harness/thinking_depth_manager.rb +564 -5
- data/lib/aidp/harness/usage_limit.rb +179 -0
- data/lib/aidp/harness/usage_limit_enforcer.rb +240 -0
- data/lib/aidp/harness/usage_limit_tracker.rb +308 -0
- data/lib/aidp/harness/usage_period.rb +234 -0
- data/lib/aidp/init/agent_instructions_generator.rb +317 -0
- data/lib/aidp/init/runner.rb +25 -4
- data/lib/aidp/interfaces/activity_monitor_interface.rb +234 -0
- data/lib/aidp/interfaces/binary_checker_interface.rb +232 -0
- data/lib/aidp/interfaces/command_executor_interface.rb +216 -0
- data/lib/aidp/interfaces/logger_interface.rb +150 -0
- data/lib/aidp/interfaces/ui_interface.rb +254 -0
- data/lib/aidp/logger.rb +25 -7
- data/lib/aidp/orchestration_adapter.rb +279 -0
- data/lib/aidp/pr_worktree_manager.rb +4 -15
- data/lib/aidp/prompts/feedback_collector.rb +198 -0
- data/lib/aidp/prompts/prompt_template_manager.rb +415 -0
- data/lib/aidp/prompts/template_evolver.rb +305 -0
- data/lib/aidp/prompts/template_version_manager.rb +325 -0
- data/lib/aidp/setup/in_memory_config_adapter.rb +257 -0
- data/lib/aidp/setup/in_memory_config_manager.rb +262 -0
- data/lib/aidp/setup/wizard.rb +616 -176
- data/lib/aidp/shell_executor.rb +58 -0
- data/lib/aidp/style_guide/selector.rb +56 -1
- data/lib/aidp/temporal/activities/analyze_issue_activity.rb +198 -0
- data/lib/aidp/temporal/activities/analyze_sub_task_activity.rb +163 -0
- data/lib/aidp/temporal/activities/base_activity.rb +78 -0
- data/lib/aidp/temporal/activities/create_plan_activity.rb +115 -0
- data/lib/aidp/temporal/activities/create_pr_activity.rb +193 -0
- data/lib/aidp/temporal/activities/create_prompt_activity.rb +118 -0
- data/lib/aidp/temporal/activities/diagnose_failure_activity.rb +228 -0
- data/lib/aidp/temporal/activities/prepare_next_iteration_activity.rb +147 -0
- data/lib/aidp/temporal/activities/record_checkpoint_activity.rb +81 -0
- data/lib/aidp/temporal/activities/run_agent_activity.rb +139 -0
- data/lib/aidp/temporal/activities/run_tests_activity.rb +114 -0
- data/lib/aidp/temporal/activities/run_work_loop_iteration_activity.rb +249 -0
- data/lib/aidp/temporal/configuration.rb +169 -0
- data/lib/aidp/temporal/connection.rb +93 -0
- data/lib/aidp/temporal/worker.rb +119 -0
- data/lib/aidp/temporal/workflow_client.rb +171 -0
- data/lib/aidp/temporal/workflows/base_workflow.rb +73 -0
- data/lib/aidp/temporal/workflows/issue_to_pr_workflow.rb +278 -0
- data/lib/aidp/temporal/workflows/sub_issue_workflow.rb +253 -0
- data/lib/aidp/temporal/workflows/work_loop_workflow.rb +321 -0
- data/lib/aidp/temporal.rb +120 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/base_processor.rb +35 -0
- data/lib/aidp/watch/build_processor.rb +132 -0
- data/lib/aidp/watch/github_state_extractor.rb +0 -8
- data/lib/aidp/watch/rebase_processor.rb +245 -0
- data/lib/aidp/watch/round_robin_scheduler.rb +150 -0
- data/lib/aidp/watch/runner.rb +366 -285
- data/lib/aidp/watch/state_store.rb +80 -0
- data/lib/aidp/watch/work_item.rb +96 -0
- data/lib/aidp/watch/worktree_cleanup_job.rb +226 -0
- data/lib/aidp/watch/worktree_reconciler.rb +643 -0
- data/lib/aidp/workflows/guided_agent.rb +1 -1
- data/lib/aidp/worktree.rb +62 -0
- data/templates/aidp.yml.example +70 -0
- metadata +117 -16
- data/lib/aidp/analyze/json_file_storage.rb +0 -292
- data/lib/aidp/execute/future_work_backlog.rb +0 -411
- data/lib/aidp/providers/adapter.rb +0 -239
- data/lib/aidp/providers/aider.rb +0 -264
- data/lib/aidp/providers/anthropic.rb +0 -614
- data/lib/aidp/providers/base.rb +0 -617
- data/lib/aidp/providers/capability_registry.rb +0 -204
- data/lib/aidp/providers/codex.rb +0 -267
- data/lib/aidp/providers/cursor.rb +0 -304
- data/lib/aidp/providers/error_taxonomy.rb +0 -195
- data/lib/aidp/providers/gemini.rb +0 -110
- data/lib/aidp/providers/github_copilot.rb +0 -227
- data/lib/aidp/providers/kilocode.rb +0 -157
- data/lib/aidp/providers/opencode.rb +0 -139
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module Aidp
|
|
7
|
+
module Temporal
|
|
8
|
+
# Configuration management for Temporal integration
|
|
9
|
+
# Loads settings from aidp.yml and environment variables
|
|
10
|
+
class Configuration
|
|
11
|
+
DEFAULT_CONFIG = {
|
|
12
|
+
enabled: false, # Disabled by default - users must explicitly opt-in
|
|
13
|
+
target_host: "localhost:7233",
|
|
14
|
+
namespace: "default",
|
|
15
|
+
task_queue: "aidp-workflows",
|
|
16
|
+
tls: false,
|
|
17
|
+
worker: {
|
|
18
|
+
max_concurrent_activities: 10,
|
|
19
|
+
max_concurrent_workflows: 10,
|
|
20
|
+
shutdown_grace_period: 30
|
|
21
|
+
},
|
|
22
|
+
timeouts: {
|
|
23
|
+
workflow_execution: 86400, # 24 hours
|
|
24
|
+
workflow_run: 3600, # 1 hour
|
|
25
|
+
activity_start_to_close: 600, # 10 minutes
|
|
26
|
+
activity_schedule_to_close: 1800, # 30 minutes
|
|
27
|
+
activity_heartbeat: 60 # 1 minute
|
|
28
|
+
},
|
|
29
|
+
retry: {
|
|
30
|
+
initial_interval: 1,
|
|
31
|
+
backoff_coefficient: 2.0,
|
|
32
|
+
maximum_interval: 60,
|
|
33
|
+
maximum_attempts: 3
|
|
34
|
+
}
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
attr_reader :config
|
|
38
|
+
|
|
39
|
+
def initialize(project_dir = Dir.pwd)
|
|
40
|
+
@project_dir = project_dir
|
|
41
|
+
@config = load_config
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Check if Temporal is enabled
|
|
45
|
+
def enabled?
|
|
46
|
+
@config[:enabled]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Get connection configuration
|
|
50
|
+
def connection_config
|
|
51
|
+
{
|
|
52
|
+
target_host: @config[:target_host],
|
|
53
|
+
namespace: @config[:namespace],
|
|
54
|
+
tls: @config[:tls],
|
|
55
|
+
api_key: @config[:api_key]
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Get worker configuration
|
|
60
|
+
def worker_config
|
|
61
|
+
{
|
|
62
|
+
task_queue: @config[:task_queue],
|
|
63
|
+
max_concurrent_activities: @config.dig(:worker, :max_concurrent_activities),
|
|
64
|
+
max_concurrent_workflows: @config.dig(:worker, :max_concurrent_workflows),
|
|
65
|
+
shutdown_grace_period: @config.dig(:worker, :shutdown_grace_period)
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Get timeout configuration
|
|
70
|
+
def timeout_config
|
|
71
|
+
@config[:timeouts] || {}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Get retry policy configuration
|
|
75
|
+
def retry_config
|
|
76
|
+
@config[:retry] || {}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Get task queue name
|
|
80
|
+
def task_queue
|
|
81
|
+
@config[:task_queue]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Get target host
|
|
85
|
+
def target_host
|
|
86
|
+
@config[:target_host]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Get namespace
|
|
90
|
+
def namespace
|
|
91
|
+
@config[:namespace]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Build a connection with this configuration
|
|
95
|
+
def build_connection
|
|
96
|
+
Connection.new(connection_config)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Build a worker with this configuration
|
|
100
|
+
def build_worker(connection: nil)
|
|
101
|
+
conn = connection || build_connection
|
|
102
|
+
Worker.new(connection: conn, config: worker_config)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Build a workflow client with this configuration
|
|
106
|
+
def build_workflow_client(connection: nil)
|
|
107
|
+
conn = connection || build_connection
|
|
108
|
+
WorkflowClient.new(connection: conn)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def load_config
|
|
114
|
+
base_config = deep_merge(DEFAULT_CONFIG.dup, load_yaml_config)
|
|
115
|
+
apply_environment_overrides(base_config)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def load_yaml_config
|
|
119
|
+
config_path = File.join(@project_dir, ".aidp", "aidp.yml")
|
|
120
|
+
return {} unless File.exist?(config_path)
|
|
121
|
+
|
|
122
|
+
full_config = YAML.safe_load_file(config_path, permitted_classes: [Date, Time, Symbol], aliases: true)
|
|
123
|
+
temporal_config = full_config["temporal"] || full_config[:temporal] || {}
|
|
124
|
+
|
|
125
|
+
symbolize_keys(temporal_config)
|
|
126
|
+
rescue => e
|
|
127
|
+
Aidp.log_warn("temporal_config", "load_failed", error: e.message)
|
|
128
|
+
{}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def apply_environment_overrides(config)
|
|
132
|
+
# Environment variables take precedence
|
|
133
|
+
config[:target_host] = ENV["TEMPORAL_HOST"] if ENV["TEMPORAL_HOST"]
|
|
134
|
+
config[:namespace] = ENV["TEMPORAL_NAMESPACE"] if ENV["TEMPORAL_NAMESPACE"]
|
|
135
|
+
config[:task_queue] = ENV["TEMPORAL_TASK_QUEUE"] if ENV["TEMPORAL_TASK_QUEUE"]
|
|
136
|
+
config[:api_key] = ENV["TEMPORAL_API_KEY"] if ENV["TEMPORAL_API_KEY"]
|
|
137
|
+
config[:enabled] = ENV["TEMPORAL_ENABLED"] != "false" if ENV.key?("TEMPORAL_ENABLED")
|
|
138
|
+
|
|
139
|
+
if ENV["TEMPORAL_TLS"]
|
|
140
|
+
config[:tls] = ENV["TEMPORAL_TLS"] == "true"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
config
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def deep_merge(base, override)
|
|
147
|
+
return base if override.nil? || override.empty?
|
|
148
|
+
|
|
149
|
+
base.merge(override) do |_key, base_val, override_val|
|
|
150
|
+
if base_val.is_a?(Hash) && override_val.is_a?(Hash)
|
|
151
|
+
deep_merge(base_val, override_val)
|
|
152
|
+
else
|
|
153
|
+
override_val.nil? ? base_val : override_val
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def symbolize_keys(hash)
|
|
159
|
+
return hash unless hash.is_a?(Hash)
|
|
160
|
+
|
|
161
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
162
|
+
new_key = key.is_a?(String) ? key.to_sym : key
|
|
163
|
+
new_value = value.is_a?(Hash) ? symbolize_keys(value) : value
|
|
164
|
+
result[new_key] = new_value
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "temporalio/client"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Temporal
|
|
7
|
+
# Manages Temporal client connections
|
|
8
|
+
# Handles connection pooling, configuration, and lifecycle
|
|
9
|
+
class Connection
|
|
10
|
+
DEFAULT_TARGET_HOST = "localhost:7233"
|
|
11
|
+
DEFAULT_NAMESPACE = "default"
|
|
12
|
+
|
|
13
|
+
attr_reader :client, :config
|
|
14
|
+
|
|
15
|
+
def initialize(config = {})
|
|
16
|
+
@config = normalize_config(config)
|
|
17
|
+
@client = nil
|
|
18
|
+
@mutex = Mutex.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Get or create a connected client
|
|
22
|
+
# Thread-safe lazy initialization
|
|
23
|
+
def connect
|
|
24
|
+
@mutex.synchronize do
|
|
25
|
+
@client ||= create_client
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Check if connected
|
|
30
|
+
def connected?
|
|
31
|
+
@mutex.synchronize { !@client.nil? }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Close the connection
|
|
35
|
+
def close
|
|
36
|
+
@mutex.synchronize do
|
|
37
|
+
@client = nil
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Get target host
|
|
42
|
+
def target_host
|
|
43
|
+
@config[:target_host]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Get namespace
|
|
47
|
+
def namespace
|
|
48
|
+
@config[:namespace]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def normalize_config(config)
|
|
54
|
+
{
|
|
55
|
+
target_host: config[:target_host] || config["target_host"] || ENV["TEMPORAL_HOST"] || DEFAULT_TARGET_HOST,
|
|
56
|
+
namespace: config[:namespace] || config["namespace"] || ENV["TEMPORAL_NAMESPACE"] || DEFAULT_NAMESPACE,
|
|
57
|
+
tls: config[:tls] || config["tls"] || false,
|
|
58
|
+
api_key: config[:api_key] || config["api_key"] || ENV["TEMPORAL_API_KEY"]
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def create_client
|
|
63
|
+
Aidp.log_debug("temporal_connection", "connecting",
|
|
64
|
+
target_host: @config[:target_host],
|
|
65
|
+
namespace: @config[:namespace],
|
|
66
|
+
tls: @config[:tls])
|
|
67
|
+
|
|
68
|
+
options = {
|
|
69
|
+
target_host: @config[:target_host],
|
|
70
|
+
namespace: @config[:namespace]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Add TLS configuration if enabled
|
|
74
|
+
if @config[:tls]
|
|
75
|
+
options[:tls] = true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Add API key if provided (for Temporal Cloud)
|
|
79
|
+
if @config[:api_key]
|
|
80
|
+
options[:api_key] = @config[:api_key]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
client = Temporalio::Client.connect(**options)
|
|
84
|
+
|
|
85
|
+
Aidp.log_info("temporal_connection", "connected",
|
|
86
|
+
target_host: @config[:target_host],
|
|
87
|
+
namespace: @config[:namespace])
|
|
88
|
+
|
|
89
|
+
client
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "temporalio/worker"
|
|
4
|
+
require_relative "connection"
|
|
5
|
+
|
|
6
|
+
module Aidp
|
|
7
|
+
module Temporal
|
|
8
|
+
# Temporal worker that runs workflows and activities
|
|
9
|
+
# Manages the worker lifecycle and task queue configuration
|
|
10
|
+
class Worker
|
|
11
|
+
DEFAULT_TASK_QUEUE = "aidp-workflows"
|
|
12
|
+
DEFAULT_MAX_CONCURRENT_ACTIVITIES = 10
|
|
13
|
+
DEFAULT_MAX_CONCURRENT_WORKFLOWS = 10
|
|
14
|
+
|
|
15
|
+
attr_reader :task_queue, :config, :connection
|
|
16
|
+
|
|
17
|
+
def initialize(connection:, config: {})
|
|
18
|
+
@connection = connection
|
|
19
|
+
@config = normalize_config(config)
|
|
20
|
+
@task_queue = @config[:task_queue]
|
|
21
|
+
@worker = nil
|
|
22
|
+
@running = false
|
|
23
|
+
@shutdown_requested = false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Register workflow classes
|
|
27
|
+
def register_workflows(*workflow_classes)
|
|
28
|
+
@workflow_classes ||= []
|
|
29
|
+
@workflow_classes.concat(workflow_classes)
|
|
30
|
+
self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Register activity classes or instances
|
|
34
|
+
def register_activities(*activity_classes)
|
|
35
|
+
@activity_classes ||= []
|
|
36
|
+
@activity_classes.concat(activity_classes)
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Start the worker (blocking)
|
|
41
|
+
def run
|
|
42
|
+
return if @running
|
|
43
|
+
|
|
44
|
+
Aidp.log_info("temporal_worker", "starting",
|
|
45
|
+
task_queue: @task_queue,
|
|
46
|
+
workflows: @workflow_classes&.map(&:name),
|
|
47
|
+
activities: @activity_classes&.map { |a| a.is_a?(Class) ? a.name : a.class.name })
|
|
48
|
+
|
|
49
|
+
@running = true
|
|
50
|
+
@shutdown_requested = false
|
|
51
|
+
|
|
52
|
+
begin
|
|
53
|
+
@worker = create_worker
|
|
54
|
+
@worker.run
|
|
55
|
+
rescue => e
|
|
56
|
+
Aidp.log_error("temporal_worker", "run_failed",
|
|
57
|
+
error: e.message,
|
|
58
|
+
error_class: e.class.name)
|
|
59
|
+
raise
|
|
60
|
+
ensure
|
|
61
|
+
@running = false
|
|
62
|
+
@worker = nil
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Request graceful shutdown
|
|
67
|
+
def shutdown
|
|
68
|
+
return unless @running
|
|
69
|
+
|
|
70
|
+
Aidp.log_info("temporal_worker", "shutdown_requested", task_queue: @task_queue)
|
|
71
|
+
@shutdown_requested = true
|
|
72
|
+
@worker&.shutdown
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Check if running
|
|
76
|
+
def running?
|
|
77
|
+
@running
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Check if shutdown was requested
|
|
81
|
+
def shutdown_requested?
|
|
82
|
+
@shutdown_requested
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def normalize_config(config)
|
|
88
|
+
{
|
|
89
|
+
task_queue: config[:task_queue] || config["task_queue"] || ENV["TEMPORAL_TASK_QUEUE"] || DEFAULT_TASK_QUEUE,
|
|
90
|
+
max_concurrent_activities: config[:max_concurrent_activities] || config["max_concurrent_activities"] || DEFAULT_MAX_CONCURRENT_ACTIVITIES,
|
|
91
|
+
max_concurrent_workflows: config[:max_concurrent_workflows] || config["max_concurrent_workflows"] || DEFAULT_MAX_CONCURRENT_WORKFLOWS
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def create_worker
|
|
96
|
+
client = @connection.connect
|
|
97
|
+
|
|
98
|
+
worker_options = {
|
|
99
|
+
client: client,
|
|
100
|
+
task_queue: @task_queue
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# Add workflows if registered
|
|
104
|
+
worker_options[:workflows] = @workflow_classes if @workflow_classes&.any?
|
|
105
|
+
|
|
106
|
+
# Add activities if registered
|
|
107
|
+
if @activity_classes&.any?
|
|
108
|
+
worker_options[:activities] = @activity_classes
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Add tuning options
|
|
112
|
+
worker_options[:max_concurrent_activity_task_polls] = @config[:max_concurrent_activities]
|
|
113
|
+
worker_options[:max_concurrent_workflow_task_polls] = @config[:max_concurrent_workflows]
|
|
114
|
+
|
|
115
|
+
Temporalio::Worker.new(**worker_options)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "connection"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Temporal
|
|
7
|
+
# Client interface for starting and managing Temporal workflows
|
|
8
|
+
# Wraps Temporal client operations with AIDP-specific conventions
|
|
9
|
+
class WorkflowClient
|
|
10
|
+
DEFAULT_WORKFLOW_TIMEOUT = 86400 # 24 hours in seconds
|
|
11
|
+
DEFAULT_TASK_QUEUE = "aidp-workflows"
|
|
12
|
+
|
|
13
|
+
attr_reader :connection
|
|
14
|
+
|
|
15
|
+
def initialize(connection:)
|
|
16
|
+
@connection = connection
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Start a workflow execution
|
|
20
|
+
# Returns workflow handle for status tracking
|
|
21
|
+
def start_workflow(workflow_class, input, options = {})
|
|
22
|
+
client = @connection.connect
|
|
23
|
+
workflow_id = options[:workflow_id] || generate_workflow_id(workflow_class)
|
|
24
|
+
task_queue = options[:task_queue] || DEFAULT_TASK_QUEUE
|
|
25
|
+
|
|
26
|
+
Aidp.log_info("workflow_client", "starting_workflow",
|
|
27
|
+
workflow: workflow_class.name,
|
|
28
|
+
workflow_id: workflow_id,
|
|
29
|
+
task_queue: task_queue)
|
|
30
|
+
|
|
31
|
+
handle = client.start_workflow(
|
|
32
|
+
workflow_class,
|
|
33
|
+
input,
|
|
34
|
+
id: workflow_id,
|
|
35
|
+
task_queue: task_queue,
|
|
36
|
+
execution_timeout: options[:execution_timeout] || DEFAULT_WORKFLOW_TIMEOUT,
|
|
37
|
+
**extract_temporal_options(options)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
Aidp.log_debug("workflow_client", "workflow_started",
|
|
41
|
+
workflow_id: workflow_id,
|
|
42
|
+
run_id: handle.result_run_id)
|
|
43
|
+
|
|
44
|
+
handle
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Execute a workflow synchronously (start and wait for result)
|
|
48
|
+
def execute_workflow(workflow_class, input, options = {})
|
|
49
|
+
handle = start_workflow(workflow_class, input, options)
|
|
50
|
+
handle.result
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Get a workflow handle by ID
|
|
54
|
+
def get_workflow(workflow_id, run_id: nil)
|
|
55
|
+
client = @connection.connect
|
|
56
|
+
client.workflow_handle(workflow_id, run_id: run_id)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Query workflow state
|
|
60
|
+
def query_workflow(workflow_id, query_name, *args)
|
|
61
|
+
handle = get_workflow(workflow_id)
|
|
62
|
+
handle.query(query_name, *args)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Signal a running workflow
|
|
66
|
+
def signal_workflow(workflow_id, signal_name, *args)
|
|
67
|
+
handle = get_workflow(workflow_id)
|
|
68
|
+
|
|
69
|
+
Aidp.log_debug("workflow_client", "signaling_workflow",
|
|
70
|
+
workflow_id: workflow_id,
|
|
71
|
+
signal: signal_name)
|
|
72
|
+
|
|
73
|
+
handle.signal(signal_name, *args)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Cancel a running workflow
|
|
77
|
+
def cancel_workflow(workflow_id)
|
|
78
|
+
handle = get_workflow(workflow_id)
|
|
79
|
+
|
|
80
|
+
Aidp.log_info("workflow_client", "canceling_workflow",
|
|
81
|
+
workflow_id: workflow_id)
|
|
82
|
+
|
|
83
|
+
handle.cancel
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Terminate a workflow
|
|
87
|
+
def terminate_workflow(workflow_id, reason: nil)
|
|
88
|
+
handle = get_workflow(workflow_id)
|
|
89
|
+
|
|
90
|
+
Aidp.log_info("workflow_client", "terminating_workflow",
|
|
91
|
+
workflow_id: workflow_id,
|
|
92
|
+
reason: reason)
|
|
93
|
+
|
|
94
|
+
handle.terminate(reason)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Get workflow result (waits for completion)
|
|
98
|
+
def get_workflow_result(workflow_id, timeout: nil)
|
|
99
|
+
handle = get_workflow(workflow_id)
|
|
100
|
+
|
|
101
|
+
if timeout
|
|
102
|
+
# Use timeout for waiting
|
|
103
|
+
Timeout.timeout(timeout) do
|
|
104
|
+
handle.result
|
|
105
|
+
end
|
|
106
|
+
else
|
|
107
|
+
handle.result
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Check if workflow is running
|
|
112
|
+
def workflow_running?(workflow_id)
|
|
113
|
+
handle = get_workflow(workflow_id)
|
|
114
|
+
desc = handle.describe
|
|
115
|
+
desc.status == :running
|
|
116
|
+
rescue Temporalio::Error::RPCError => e
|
|
117
|
+
# NOT_FOUND means workflow doesn't exist
|
|
118
|
+
return false if e.code == Temporalio::Error::RPCError::Code::NOT_FOUND
|
|
119
|
+
|
|
120
|
+
raise
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# List workflows with optional filters
|
|
124
|
+
def list_workflows(query: nil, page_size: 100)
|
|
125
|
+
client = @connection.connect
|
|
126
|
+
|
|
127
|
+
options = {page_size: page_size}
|
|
128
|
+
options[:query] = query if query
|
|
129
|
+
|
|
130
|
+
client.list_workflows(**options)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
def generate_workflow_id(workflow_class)
|
|
136
|
+
timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
|
|
137
|
+
random = SecureRandom.hex(4)
|
|
138
|
+
workflow_name = workflow_class.name.split("::").last.gsub(/Workflow$/, "").downcase
|
|
139
|
+
"#{workflow_name}_#{timestamp}_#{random}"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def extract_temporal_options(options)
|
|
143
|
+
temporal_opts = {}
|
|
144
|
+
|
|
145
|
+
# Timeout options
|
|
146
|
+
temporal_opts[:run_timeout] = options[:run_timeout] if options[:run_timeout]
|
|
147
|
+
temporal_opts[:task_timeout] = options[:task_timeout] if options[:task_timeout]
|
|
148
|
+
|
|
149
|
+
# Retry policy
|
|
150
|
+
if options[:retry_policy]
|
|
151
|
+
temporal_opts[:retry_policy] = build_retry_policy(options[:retry_policy])
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Memo and search attributes
|
|
155
|
+
temporal_opts[:memo] = options[:memo] if options[:memo]
|
|
156
|
+
temporal_opts[:search_attributes] = options[:search_attributes] if options[:search_attributes]
|
|
157
|
+
|
|
158
|
+
temporal_opts
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def build_retry_policy(policy_config)
|
|
162
|
+
Temporalio::RetryPolicy.new(
|
|
163
|
+
initial_interval: policy_config[:initial_interval] || 1,
|
|
164
|
+
backoff_coefficient: policy_config[:backoff_coefficient] || 2.0,
|
|
165
|
+
maximum_interval: policy_config[:maximum_interval] || 60,
|
|
166
|
+
maximum_attempts: policy_config[:maximum_attempts] || 3
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "temporalio/workflow"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Temporal
|
|
7
|
+
module Workflows
|
|
8
|
+
# Base class for AIDP Temporal workflows
|
|
9
|
+
# Provides common patterns and utilities for workflow implementations
|
|
10
|
+
class BaseWorkflow < Temporalio::Workflow::Definition
|
|
11
|
+
# Default activity options applied to all activities
|
|
12
|
+
DEFAULT_ACTIVITY_OPTIONS = {
|
|
13
|
+
start_to_close_timeout: 600, # 10 minutes
|
|
14
|
+
heartbeat_timeout: 60, # 1 minute
|
|
15
|
+
retry_policy: {
|
|
16
|
+
initial_interval: 1,
|
|
17
|
+
backoff_coefficient: 2.0,
|
|
18
|
+
maximum_interval: 60,
|
|
19
|
+
maximum_attempts: 3
|
|
20
|
+
}
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
# Define workflow name from class name
|
|
25
|
+
def workflow_name
|
|
26
|
+
name.split("::").last.gsub(/Workflow$/, "").gsub(/([A-Z])/) { "_#{$1.downcase}" }.sub(/^_/, "")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Helper to define activity with default options
|
|
30
|
+
def activity_options(overrides = {})
|
|
31
|
+
DEFAULT_ACTIVITY_OPTIONS.merge(overrides)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
protected
|
|
36
|
+
|
|
37
|
+
# Log within workflow context
|
|
38
|
+
def log_workflow(action, **context)
|
|
39
|
+
workflow_info = Temporalio::Workflow.info
|
|
40
|
+
Aidp.log_debug("temporal_workflow", action,
|
|
41
|
+
workflow_id: workflow_info.workflow_id,
|
|
42
|
+
run_id: workflow_info.run_id,
|
|
43
|
+
**context)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Sleep that respects cancellation
|
|
47
|
+
def workflow_sleep(duration)
|
|
48
|
+
Temporalio::Workflow.sleep(duration)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Get current workflow info
|
|
52
|
+
def workflow_info
|
|
53
|
+
Temporalio::Workflow.info
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Check if cancellation was requested
|
|
57
|
+
def cancellation_requested?
|
|
58
|
+
Temporalio::Workflow.cancellation_pending?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Build retry policy from config
|
|
62
|
+
def build_retry_policy(config)
|
|
63
|
+
Temporalio::RetryPolicy.new(
|
|
64
|
+
initial_interval: config[:initial_interval] || 1,
|
|
65
|
+
backoff_coefficient: config[:backoff_coefficient] || 2.0,
|
|
66
|
+
maximum_interval: config[:maximum_interval] || 60,
|
|
67
|
+
maximum_attempts: config[:maximum_attempts] || 3
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|