aidp 0.47.1 → 0.48.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/lib/aidp/cli/temporal_command.rb +132 -3
- data/lib/aidp/database/repositories/experience_artifact_repository.rb +40 -0
- data/lib/aidp/database/repositories/experience_evaluation_repository.rb +55 -0
- data/lib/aidp/database/repositories/experience_run_repository.rb +143 -0
- data/lib/aidp/database/repositories/experience_task_repository.rb +62 -0
- data/lib/aidp/database/repositories/strategy_repository.rb +59 -0
- data/lib/aidp/database/schema.rb +95 -1
- data/lib/aidp/database.rb +38 -0
- data/lib/aidp/strategy_execution/cli_protocol.rb +119 -0
- data/lib/aidp/strategy_execution/experience_store.rb +155 -0
- data/lib/aidp/strategy_execution/strategy_loader.rb +26 -0
- data/lib/aidp/strategy_execution/strategy_spec.rb +127 -0
- data/lib/aidp/temporal/activities/execute_cli_command_activity.rb +51 -0
- data/lib/aidp/temporal/activities/manage_experience_store_activity.rb +56 -0
- data/lib/aidp/temporal/workflows/strategy_branch_workflow.rb +294 -0
- data/lib/aidp/temporal/workflows/strategy_execution_workflow.rb +294 -0
- data/lib/aidp/temporal.rb +8 -0
- data/lib/aidp/version.rb +1 -1
- metadata +14 -1
data/lib/aidp/database.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "sqlite3"
|
|
4
4
|
require "json"
|
|
5
5
|
require "fileutils"
|
|
6
|
+
Kernel.require("set") unless defined?(Set)
|
|
6
7
|
|
|
7
8
|
module Aidp
|
|
8
9
|
# Database module for SQLite-based storage
|
|
@@ -15,6 +16,8 @@ module Aidp
|
|
|
15
16
|
# Thread-safe connection cache
|
|
16
17
|
@connections = {}
|
|
17
18
|
@mutex = Mutex.new
|
|
19
|
+
@migrated_projects = Set.new
|
|
20
|
+
@migration_mutex = Mutex.new
|
|
18
21
|
|
|
19
22
|
class << self
|
|
20
23
|
# Get or create a database connection for the given project directory
|
|
@@ -26,6 +29,8 @@ module Aidp
|
|
|
26
29
|
db_path = ConfigPaths.database_file(project_dir)
|
|
27
30
|
|
|
28
31
|
@mutex.synchronize do
|
|
32
|
+
invalidate_missing_connection!(db_path)
|
|
33
|
+
|
|
29
34
|
# Return cached connection if valid
|
|
30
35
|
if @connections[db_path]&.closed? == false
|
|
31
36
|
return @connections[db_path]
|
|
@@ -53,6 +58,23 @@ module Aidp
|
|
|
53
58
|
Migrations.run!(project_dir)
|
|
54
59
|
end
|
|
55
60
|
|
|
61
|
+
# Run pending migrations at most once per process for each project directory.
|
|
62
|
+
#
|
|
63
|
+
# @param project_dir [String] Project directory path
|
|
64
|
+
# @return [Array<Integer>] List of applied migration versions
|
|
65
|
+
def migrate_once!(project_dir = Dir.pwd)
|
|
66
|
+
expanded_project_dir = File.expand_path(project_dir)
|
|
67
|
+
require_relative "database/migrations"
|
|
68
|
+
|
|
69
|
+
@migration_mutex.synchronize do
|
|
70
|
+
return [] if migration_current?(expanded_project_dir)
|
|
71
|
+
|
|
72
|
+
migrate!(expanded_project_dir).tap do
|
|
73
|
+
@migrated_projects << expanded_project_dir
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
56
78
|
# Check if database exists and is initialized
|
|
57
79
|
#
|
|
58
80
|
# @param project_dir [String] Project directory path
|
|
@@ -134,6 +156,22 @@ module Aidp
|
|
|
134
156
|
# Set busy timeout to 5 seconds
|
|
135
157
|
db.busy_timeout = 5000
|
|
136
158
|
end
|
|
159
|
+
|
|
160
|
+
def invalidate_missing_connection!(db_path)
|
|
161
|
+
db = @connections[db_path]
|
|
162
|
+
return unless db
|
|
163
|
+
return if db.closed?
|
|
164
|
+
return if File.exist?(db_path)
|
|
165
|
+
|
|
166
|
+
db.close
|
|
167
|
+
@connections.delete(db_path)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def migration_current?(project_dir)
|
|
171
|
+
@migrated_projects.include?(project_dir) &&
|
|
172
|
+
exists?(project_dir) &&
|
|
173
|
+
!Migrations.pending?(project_dir)
|
|
174
|
+
end
|
|
137
175
|
end
|
|
138
176
|
end
|
|
139
177
|
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "open3"
|
|
5
|
+
require "shellwords"
|
|
6
|
+
require "tmpdir"
|
|
7
|
+
require "fileutils"
|
|
8
|
+
require "pathname"
|
|
9
|
+
|
|
10
|
+
module Aidp
|
|
11
|
+
module StrategyExecution
|
|
12
|
+
module CliProtocol
|
|
13
|
+
class ProtocolError < StandardError; end
|
|
14
|
+
|
|
15
|
+
class Runner
|
|
16
|
+
PROTOCOL_VERSION = "1.0"
|
|
17
|
+
|
|
18
|
+
def initialize(project_dir: Dir.pwd)
|
|
19
|
+
@project_dir = project_dir
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def execute(command:, role:, request:)
|
|
23
|
+
parts = command_parts(command)
|
|
24
|
+
raise ProtocolError, "command must not be blank" if parts.empty?
|
|
25
|
+
|
|
26
|
+
artifact_dir = create_artifact_dir(role)
|
|
27
|
+
payload = request.merge(
|
|
28
|
+
protocol_version: PROTOCOL_VERSION,
|
|
29
|
+
role: role,
|
|
30
|
+
artifact_dir: artifact_dir
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
Aidp.log_debug("cli_protocol", "executing",
|
|
34
|
+
role: role,
|
|
35
|
+
command: parts.first,
|
|
36
|
+
task_id: request[:task]&.dig(:id),
|
|
37
|
+
artifact_dir: artifact_dir)
|
|
38
|
+
|
|
39
|
+
stdout, stderr, status = Open3.capture3(*parts, stdin_data: JSON.generate(payload), chdir: @project_dir)
|
|
40
|
+
unless status.success?
|
|
41
|
+
Aidp.log_error("cli_protocol", "execution_failed",
|
|
42
|
+
role: role,
|
|
43
|
+
command: parts.first,
|
|
44
|
+
exit_status: status.exitstatus,
|
|
45
|
+
stderr: stderr.to_s.strip)
|
|
46
|
+
raise ProtocolError, stderr.strip
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
response = JSON.parse(stdout, symbolize_names: true)
|
|
50
|
+
validate_response!(response, role)
|
|
51
|
+
|
|
52
|
+
response.merge(
|
|
53
|
+
artifacts: normalize_artifacts(response[:artifacts], artifact_dir),
|
|
54
|
+
artifact_dir: artifact_dir,
|
|
55
|
+
stderr: stderr
|
|
56
|
+
)
|
|
57
|
+
rescue JSON::ParserError => e
|
|
58
|
+
Aidp.log_error("cli_protocol", "invalid_response",
|
|
59
|
+
role: role,
|
|
60
|
+
error: e.message)
|
|
61
|
+
raise ProtocolError, "Invalid JSON response: #{e.message}"
|
|
62
|
+
rescue ProtocolError
|
|
63
|
+
raise
|
|
64
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
|
65
|
+
Aidp.log_error("cli_protocol", "execution_failed",
|
|
66
|
+
role: role,
|
|
67
|
+
command: parts.first,
|
|
68
|
+
error: e.message,
|
|
69
|
+
error_class: e.class.name)
|
|
70
|
+
raise ProtocolError, "Command failed: #{e.message}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def command_parts(command)
|
|
76
|
+
case command
|
|
77
|
+
when Array then command
|
|
78
|
+
else Shellwords.split(command.to_s)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def create_artifact_dir(role)
|
|
83
|
+
base_dir = File.join(@project_dir, ".aidp")
|
|
84
|
+
FileUtils.mkdir_p(base_dir)
|
|
85
|
+
dir = Dir.mktmpdir("aidp-#{role}-", base_dir)
|
|
86
|
+
FileUtils.mkdir_p(dir)
|
|
87
|
+
dir
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def normalize_artifacts(artifacts, artifact_dir)
|
|
91
|
+
expanded_artifact_dir = File.expand_path(artifact_dir)
|
|
92
|
+
|
|
93
|
+
Array(artifacts).map do |artifact|
|
|
94
|
+
next artifact unless artifact.is_a?(String)
|
|
95
|
+
|
|
96
|
+
expanded_path = File.expand_path(artifact, expanded_artifact_dir)
|
|
97
|
+
validate_artifact_path!(artifact, expanded_path, expanded_artifact_dir)
|
|
98
|
+
expanded_path
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def validate_artifact_path!(artifact, expanded_path, artifact_dir)
|
|
103
|
+
return if expanded_path == artifact_dir
|
|
104
|
+
return if expanded_path.start_with?("#{artifact_dir}#{File::SEPARATOR}")
|
|
105
|
+
|
|
106
|
+
raise ProtocolError, "artifact path escapes artifact directory: #{artifact}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def validate_response!(response, role)
|
|
110
|
+
raise ProtocolError, "response must include success" unless response.key?(:success)
|
|
111
|
+
return unless role == "evaluator"
|
|
112
|
+
return if response.key?(:score) || response.key?(:passed)
|
|
113
|
+
|
|
114
|
+
raise ProtocolError, "evaluator response must include score or passed"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../database"
|
|
5
|
+
require_relative "../database/repositories/strategy_repository"
|
|
6
|
+
require_relative "../database/repositories/experience_task_repository"
|
|
7
|
+
require_relative "../database/repositories/experience_run_repository"
|
|
8
|
+
require_relative "../database/repositories/experience_evaluation_repository"
|
|
9
|
+
require_relative "../database/repositories/experience_artifact_repository"
|
|
10
|
+
|
|
11
|
+
module Aidp
|
|
12
|
+
module StrategyExecution
|
|
13
|
+
class ExperienceStore
|
|
14
|
+
def initialize(project_dir: Dir.pwd)
|
|
15
|
+
@project_dir = project_dir
|
|
16
|
+
Aidp::Database.migrate_once!(@project_dir)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def register_strategy(strategy_spec)
|
|
20
|
+
record = strategy_repository.register(
|
|
21
|
+
name: strategy_spec.name,
|
|
22
|
+
spec: JSON.generate(strategy_spec.to_h)
|
|
23
|
+
)
|
|
24
|
+
Aidp.log_debug("experience_store", "strategy_registered",
|
|
25
|
+
strategy_id: record[:id], name: strategy_spec.name)
|
|
26
|
+
record
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def create_task(description:, title: nil, input_payload: {}, context: {}, source_run_id: nil)
|
|
30
|
+
task = task_repository.create(
|
|
31
|
+
description: description,
|
|
32
|
+
title: title,
|
|
33
|
+
input_payload: input_payload,
|
|
34
|
+
context: context,
|
|
35
|
+
source_run_id: source_run_id
|
|
36
|
+
)
|
|
37
|
+
Aidp.log_debug("experience_store", "task_created",
|
|
38
|
+
task_id: task[:id], source_run_id: source_run_id)
|
|
39
|
+
task
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def start_run(task_id:, strategy_id:, workflow_id:, depth:, input_payload:, parent_run_id: nil, branch_key: nil, metadata: {})
|
|
43
|
+
run = run_repository.start(
|
|
44
|
+
task_id: task_id,
|
|
45
|
+
strategy_id: strategy_id,
|
|
46
|
+
workflow_id: workflow_id,
|
|
47
|
+
depth: depth,
|
|
48
|
+
input_payload: input_payload,
|
|
49
|
+
parent_run_id: parent_run_id,
|
|
50
|
+
branch_key: branch_key,
|
|
51
|
+
metadata: metadata
|
|
52
|
+
)
|
|
53
|
+
Aidp.log_debug("experience_store", "run_started",
|
|
54
|
+
run_id: run[:id],
|
|
55
|
+
task_id: task_id,
|
|
56
|
+
strategy_id: strategy_id,
|
|
57
|
+
branch_key: branch_key,
|
|
58
|
+
depth: depth)
|
|
59
|
+
run
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def complete_run(run_id:, status:, output_payload:, metadata: {})
|
|
63
|
+
run = run_repository.complete(
|
|
64
|
+
run_id: run_id,
|
|
65
|
+
status: status,
|
|
66
|
+
output_payload: output_payload,
|
|
67
|
+
metadata: metadata
|
|
68
|
+
)
|
|
69
|
+
Aidp.log_debug("experience_store", "run_completed",
|
|
70
|
+
run_id: run_id, status: status)
|
|
71
|
+
run
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def record_evaluation(run_id:, evaluator_name:, score:, passed:, summary: nil, metadata: {})
|
|
75
|
+
evaluation_repository.record(
|
|
76
|
+
run_id: run_id,
|
|
77
|
+
evaluator_name: evaluator_name,
|
|
78
|
+
score: score,
|
|
79
|
+
passed: passed,
|
|
80
|
+
summary: summary,
|
|
81
|
+
metadata: metadata
|
|
82
|
+
)
|
|
83
|
+
Aidp.log_debug("experience_store", "evaluation_recorded",
|
|
84
|
+
run_id: run_id,
|
|
85
|
+
evaluator_name: evaluator_name,
|
|
86
|
+
score: score,
|
|
87
|
+
passed: passed)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def record_artifact(run_id:, role:, path:, metadata: {})
|
|
91
|
+
artifact_repository.record(
|
|
92
|
+
run_id: run_id,
|
|
93
|
+
role: role,
|
|
94
|
+
path: path,
|
|
95
|
+
metadata: metadata
|
|
96
|
+
)
|
|
97
|
+
Aidp.log_debug("experience_store", "artifact_recorded",
|
|
98
|
+
run_id: run_id, role: role, path: path)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def replay_bundle(run_id)
|
|
102
|
+
bundle = run_repository.bundle_for_replay(run_id)
|
|
103
|
+
Aidp.log_debug("experience_store", "replay_bundle_loaded",
|
|
104
|
+
run_id: run_id, found: !bundle.nil?)
|
|
105
|
+
bundle
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def task_details(task_id)
|
|
109
|
+
task = task_repository.load(task_id)
|
|
110
|
+
Aidp.log_debug("experience_store", "task_details_loaded",
|
|
111
|
+
task_id: task_id, found: !task.nil?)
|
|
112
|
+
task
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def run_details(run_id)
|
|
116
|
+
run = run_repository.load(run_id)
|
|
117
|
+
return nil unless run
|
|
118
|
+
|
|
119
|
+
details = run.merge(
|
|
120
|
+
evaluations: evaluation_repository.list_for_run(run_id),
|
|
121
|
+
artifacts: artifact_repository.list_for_run(run_id),
|
|
122
|
+
children: run_repository.children(run_id)
|
|
123
|
+
)
|
|
124
|
+
Aidp.log_debug("experience_store", "run_details_loaded",
|
|
125
|
+
run_id: run_id,
|
|
126
|
+
evaluation_count: details[:evaluations].length,
|
|
127
|
+
artifact_count: details[:artifacts].length,
|
|
128
|
+
child_count: details[:children].length)
|
|
129
|
+
details
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
def strategy_repository
|
|
135
|
+
@strategy_repository ||= Aidp::Database::Repositories::StrategyRepository.new(project_dir: @project_dir)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def task_repository
|
|
139
|
+
@task_repository ||= Aidp::Database::Repositories::ExperienceTaskRepository.new(project_dir: @project_dir)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def run_repository
|
|
143
|
+
@run_repository ||= Aidp::Database::Repositories::ExperienceRunRepository.new(project_dir: @project_dir)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def evaluation_repository
|
|
147
|
+
@evaluation_repository ||= Aidp::Database::Repositories::ExperienceEvaluationRepository.new(project_dir: @project_dir)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def artifact_repository
|
|
151
|
+
@artifact_repository ||= Aidp::Database::Repositories::ExperienceArtifactRepository.new(project_dir: @project_dir)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "yaml"
|
|
5
|
+
require_relative "strategy_spec"
|
|
6
|
+
|
|
7
|
+
module Aidp
|
|
8
|
+
module StrategyExecution
|
|
9
|
+
class StrategyLoader
|
|
10
|
+
def initialize(project_dir: Dir.pwd)
|
|
11
|
+
@project_dir = project_dir
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def load_file(path)
|
|
15
|
+
raw = YAML.safe_load_file(expand_path(path), permitted_classes: [Date, Time, Symbol], aliases: true) || {}
|
|
16
|
+
StrategySpec.from_hash(raw)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def expand_path(path)
|
|
22
|
+
File.expand_path(path, @project_dir)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aidp
|
|
4
|
+
module StrategyExecution
|
|
5
|
+
class StrategySpec
|
|
6
|
+
DEFAULT_MAX_DEPTH = 1
|
|
7
|
+
DEFAULT_FANOUT = 1
|
|
8
|
+
DEFAULT_MERGE_POLICY = "highest_score"
|
|
9
|
+
|
|
10
|
+
attr_reader :name, :max_depth, :fanout, :agents, :evaluators, :merge_policy, :constraints, :timeouts
|
|
11
|
+
|
|
12
|
+
def self.from_hash(data)
|
|
13
|
+
new(data.transform_keys(&:to_sym))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(data)
|
|
17
|
+
@name = fetch_name(data)
|
|
18
|
+
@max_depth = positive_integer(data[:max_depth], DEFAULT_MAX_DEPTH)
|
|
19
|
+
@fanout = positive_integer(data[:fanout], DEFAULT_FANOUT)
|
|
20
|
+
@agents = normalize_agents(data[:agents] || {})
|
|
21
|
+
@evaluators = normalize_evaluators(data[:evaluators] || [])
|
|
22
|
+
@merge_policy = (data[:merge_policy] || DEFAULT_MERGE_POLICY).to_s
|
|
23
|
+
@constraints = (data[:constraints] || {}).transform_keys(&:to_sym)
|
|
24
|
+
@timeouts = (data[:timeouts] || {}).transform_keys(&:to_sym)
|
|
25
|
+
ensure_runnable!
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_h
|
|
29
|
+
{
|
|
30
|
+
name: name,
|
|
31
|
+
max_depth: max_depth,
|
|
32
|
+
fanout: fanout,
|
|
33
|
+
agents: agents,
|
|
34
|
+
evaluators: evaluators,
|
|
35
|
+
merge_policy: merge_policy,
|
|
36
|
+
constraints: constraints,
|
|
37
|
+
timeouts: timeouts
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def branch_commands
|
|
42
|
+
commands = candidate_agent_commands
|
|
43
|
+
Array.new(fanout) do |index|
|
|
44
|
+
command = commands[index % commands.length]
|
|
45
|
+
{
|
|
46
|
+
key: "branch_#{index}",
|
|
47
|
+
name: command[:name],
|
|
48
|
+
command: command[:command]
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def evaluator_definitions
|
|
54
|
+
evaluators
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def fetch_name(data)
|
|
60
|
+
value = data[:name].to_s.strip
|
|
61
|
+
raise ArgumentError, "strategy name is required" if value.empty?
|
|
62
|
+
|
|
63
|
+
value
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def positive_integer(value, fallback)
|
|
67
|
+
number = value.to_i
|
|
68
|
+
number.positive? ? number : fallback
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def normalize_agents(agents)
|
|
72
|
+
agents.each_with_object({}) do |(role, value), normalized|
|
|
73
|
+
normalized[role.to_sym] = case value
|
|
74
|
+
when Array
|
|
75
|
+
value.map { |item| normalize_agent_entry(role, item) }
|
|
76
|
+
else
|
|
77
|
+
normalize_agent_entry(role, value)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def normalize_agent_entry(role, value)
|
|
83
|
+
case value
|
|
84
|
+
when Hash
|
|
85
|
+
{
|
|
86
|
+
name: (value[:name] || value["name"] || role).to_s,
|
|
87
|
+
command: (value[:command] || value["command"]).to_s
|
|
88
|
+
}
|
|
89
|
+
else
|
|
90
|
+
{
|
|
91
|
+
name: role.to_s,
|
|
92
|
+
command: value.to_s
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def normalize_evaluators(evaluators)
|
|
98
|
+
Array(evaluators).map do |entry|
|
|
99
|
+
case entry
|
|
100
|
+
when Hash
|
|
101
|
+
{
|
|
102
|
+
name: (entry[:name] || entry["name"]).to_s,
|
|
103
|
+
command: (entry[:command] || entry["command"] || entry[:name] || entry["name"]).to_s
|
|
104
|
+
}
|
|
105
|
+
else
|
|
106
|
+
{
|
|
107
|
+
name: entry.to_s,
|
|
108
|
+
command: entry.to_s
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def candidate_agent_commands
|
|
115
|
+
raw = agents[:coder] || agents[:execution] || agents.values
|
|
116
|
+
commands = raw.is_a?(Hash) ? [raw] : Array(raw).flatten
|
|
117
|
+
commands.reject { |entry| entry[:command].to_s.empty? }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def ensure_runnable!
|
|
121
|
+
return unless candidate_agent_commands.empty?
|
|
122
|
+
|
|
123
|
+
raise ArgumentError, "strategy '#{name}' requires at least one agent with a non-empty command"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_activity"
|
|
4
|
+
require_relative "../../strategy_execution/cli_protocol"
|
|
5
|
+
|
|
6
|
+
module Aidp
|
|
7
|
+
module Temporal
|
|
8
|
+
module Activities
|
|
9
|
+
class ExecuteCliCommandActivity < BaseActivity
|
|
10
|
+
def execute(input)
|
|
11
|
+
with_activity_context do
|
|
12
|
+
project_dir = input[:project_dir]
|
|
13
|
+
command = input[:command]
|
|
14
|
+
role = input[:role]
|
|
15
|
+
request = input[:request] || {}
|
|
16
|
+
|
|
17
|
+
runner = Aidp::StrategyExecution::CliProtocol::Runner.new(project_dir: project_dir)
|
|
18
|
+
|
|
19
|
+
# The CLI invocation blocks for the full duration of the external
|
|
20
|
+
# agent/evaluator (often minutes). Heartbeat on a background thread so
|
|
21
|
+
# Temporal does not mark the activity timed out and retry the
|
|
22
|
+
# side-effecting command while the original subprocess is still running.
|
|
23
|
+
heartbeat_thread = start_heartbeat_thread(role: role)
|
|
24
|
+
begin
|
|
25
|
+
runner.execute(command: command, role: role, request: request)
|
|
26
|
+
ensure
|
|
27
|
+
heartbeat_thread&.kill
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# Interval between heartbeats; kept well below the workflow's
|
|
35
|
+
# heartbeat_timeout so a missed beat does not time out the activity.
|
|
36
|
+
def heartbeat_interval_seconds
|
|
37
|
+
30
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def start_heartbeat_thread(role:)
|
|
41
|
+
Thread.new do
|
|
42
|
+
loop do
|
|
43
|
+
sleep heartbeat_interval_seconds
|
|
44
|
+
heartbeat(role: role, status: "running")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_activity"
|
|
4
|
+
require_relative "../../strategy_execution/experience_store"
|
|
5
|
+
require_relative "../../strategy_execution/strategy_spec"
|
|
6
|
+
|
|
7
|
+
module Aidp
|
|
8
|
+
module Temporal
|
|
9
|
+
module Activities
|
|
10
|
+
class ManageExperienceStoreActivity < BaseActivity
|
|
11
|
+
def execute(input)
|
|
12
|
+
with_activity_context do
|
|
13
|
+
project_dir = input[:project_dir]
|
|
14
|
+
operation = input[:operation]
|
|
15
|
+
payload = input[:payload] || {}
|
|
16
|
+
store = Aidp::StrategyExecution::ExperienceStore.new(project_dir: project_dir)
|
|
17
|
+
|
|
18
|
+
Aidp.log_debug("manage_experience_store_activity", "dispatching",
|
|
19
|
+
operation: operation,
|
|
20
|
+
run_id: payload[:run_id],
|
|
21
|
+
task_id: payload[:task_id],
|
|
22
|
+
strategy_id: payload[:strategy_id])
|
|
23
|
+
|
|
24
|
+
case operation
|
|
25
|
+
when "register_strategy"
|
|
26
|
+
strategy = Aidp::StrategyExecution::StrategySpec.from_hash(payload[:strategy])
|
|
27
|
+
store.register_strategy(strategy)
|
|
28
|
+
when "create_task"
|
|
29
|
+
store.create_task(**payload)
|
|
30
|
+
when "start_run"
|
|
31
|
+
store.start_run(**payload)
|
|
32
|
+
when "complete_run"
|
|
33
|
+
store.complete_run(**payload)
|
|
34
|
+
when "record_evaluation"
|
|
35
|
+
store.record_evaluation(**payload)
|
|
36
|
+
success_result
|
|
37
|
+
when "record_artifact"
|
|
38
|
+
store.record_artifact(**payload)
|
|
39
|
+
success_result
|
|
40
|
+
when "task_details"
|
|
41
|
+
store.task_details(payload[:task_id])
|
|
42
|
+
when "replay_bundle"
|
|
43
|
+
store.replay_bundle(payload[:run_id])
|
|
44
|
+
when "run_details"
|
|
45
|
+
store.run_details(payload[:run_id])
|
|
46
|
+
else
|
|
47
|
+
Aidp.log_error("manage_experience_store_activity", "unknown_operation",
|
|
48
|
+
operation: operation)
|
|
49
|
+
error_result("Unknown operation: #{operation}")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|